Introduction

Seriously speaking, there are quite a few resources out there for you to study, like NEAR University Certification Program, or the Learn Near Club LNC. Even the NEAR docs provides lots of useful information. Other partners that teaches resources includes Figment Learn and NEAR Academy. For more information, check out the education page of NEAR official website.

The problem is, there are just too much things, and everything is everywhere. There are no proper entry point for you to start with. You can imagine dumping information at you and trying to digesting everything.

Second, the certification course is going to be difficult. In fact, they requires you to have some (as in, few years is optimal, but at least 1 year if you're a great learner, or maybe less?) web development experiences to understand it quickly. It's also a 5 days workshop for Live (self-paced can be longer than that). It certainly isn't a good starting point for beginners like myself, that have no previous web development experience, been a developer in backend that just graduated from school and not even worked yet (though I suspect I have several years of development experience, individually), coded only in Python up till then.

Hence, this book aims to provide a starting point to people similar to my position. I hope it will do you good. Hopefully this book will continually be updated in the future.

This book assumes you have knowledge of Rust. Although while one is writing this book, one is still learning from the official Rust book (and some other books) about Rust. Hence, you either know about Rust to go through this easy, or you believe your searching skill is superb and be able to search through them with ease, which you can also pass through them easy. One suggests these books for Rust:

  • The Rust Book: The official Rust book containing concepts of Rust. Best read front to end without skipping chapters. (Of course, you can skip chapters you already know, up to you).

  • Rust By Example: Although the official Rust book is good for understanding concepts, it's a little too little in how to put things to practical use. This book contains the examples, and shows you how it should be used in code, rather than just pure text speaking about its pros and cons and bits and pieces. It's more complete with examples. Best used for references when you need to find a chapter. E.g. if one wants to know how to use lifetimes in Rust, one can Google: "Rust by Example Lifetimes".

  • Easy Rust intended for non-native English Speakers. You can start here if you want.

For feedback, you're welcome to raise issues at the github issues corresponding to this book. Thanks.

Also note that, some of the contents might be clone from certain websites. Kudos goes to the references of the website, which you can visit them on the References at the end of every section.

Also

Check out the Appendices for more information if you haven't know about Blockchain, and why do we want to use blockchain. Example, in the "Other Notable Resources", under "Token Economics", that whole chapter discusses about why blockchain is the future, and why it is replacing the infrastructure and organization we have now, in what ways it's better, etc.

Tips and Tricks

Read, read, and read, lots of code. Learning any programming skills in general, reading code is the best way to learn. Notice when you read some code, you like how it's structured, and some code you don't like how it's structured. Other times you notice you like/don't like how the programming language express itself. There are also code that are written badly compared to those that write good, that makes you happy reading and writing about it in that manner. Most importantly, there are a lot of tips and shortcuts and how to write a program shown in lots of different codes. By reading them, you understand them and able to put them into words yourself.

And don't just read them. Copy them as well. In fact, copy them (WITHOUT USING CTRL+C CTRL+V, unless it's repeating stuffs that's not useful like list of strings, dictionaries, that store data) by typing them out, carefully understand your way through. We do not expect you to understand everything at the beginning. In fact, when one learn something new, it is through copying similar things many times from different sources to understand a thing slowly, not one-off understanding. So, don't worry too much if you can't understand everything at once. Just copy it, and once you met the same situation n number of times, you'll get how to write the program, even though at that time you might still not understand everything, at which you could then research more deeply into how the logic works out.

Repetition: when you learn, it's usual to encounter the same thing many times, especially basic things. These serve as your repetition if you don't want to revisit the same article you read more than once (like myself). The moment you feel disgusted by a certain topic, most probably you'd known it quite well already, at least on the surface. That's when you know you can safely skip over the stuffs if you encounter it again.

Concentration: You think you have three monitor screens, and you can move from one thing to another thing like spiderman quickly is the way to go, heh! No way. Your attention is split between all the stuffs that demand your attention at the same time. Learning is about focusing your full attention on an object. That way, you could learn it quickest and most deeply understanding it. In fact, that might make your work go quicker rather than switching back and forth. If you work on one, finish, then move on to another, try it, and it'll be done quicker.

Also note that you might want to turn off your notifications (sound/vibration) when working, as it might be tempting to look at your phones for the latest news or messages rather than focusing on learning.

Conclusion

Of course, these are just my opinions. However you work, that's up to you.

Send me a Tip?

Like this book? Consider sending me some tips?

The easiest way is just sending NEAR to my address "wabinab.near" (that's the address, unlike Ethereum's address to be non-human readable).

You can also choose to tip me via twitter on any random tweets (or follow the link in the next paragraph and tip that tweet). That requires you to install NEAR tipping dapplet. Though as of writing, an extra 0.15N will go to the developers for maintenance and development of the app. One isn't sure how this works, though, as one never used it before.

How to tip on Twitter

DON'T

Don't tip with BAT, although you might see it verified. There's some problem with setting up the thing, particularly ".nojekyll" is required in github pages, but using it caused the website to go 404 (and you bet, removing it will take it back). So, one isn't sure whether the tip will gone missing forever or pass through. On the safe side, ignore Brave tipping unless you directly tip it on any of my github repo.

Thanks

One therefore thank you for your support and your interest in the book. Enjoy!

Tipping original author?

And if you want to send tip to the original author (where one took the materials from) instead, you can send nLearns for those "references" that link to Learnnear.club. Though you would need nLearn to tip.

For the others, you might want to search around whether they accept tips or not and how they accept it. These are not necessarily in NEAR.

I could be wrong

And the last page before we dive into the ideas, one would like to mention that, one could be wrong. The idea that are listed in the book are based on one's understanding of the code, and this understanding isn't necessary correct. If you found another explanation, you're welcome to explain it in the Issues or Discussion page. You are welcome to challenge me to let me know that I'm wrong, and I can alter my thoughts if you're right.

#TODO: Note to oneself, Discussion is only available for Public Repo. Remember to add it when making repo public.

Concepts

As an intro, the first thing you need to understand is how things work (the concept), rather than starting straight programming. Ultimately, learning these up front (to a certain extent) helps a lot with understanding what the program is trying to do.

There are lots of guide available in LearnNear Club (LNC) that demonstrates these principles. You might want to take a look at them. There's no particular order in which you want to learn it. Particularly, some articles are easier to understand, more beginner-friendly; others are more advanced, and doesn't expressed as well as other articles.

Let's get into it on the surface with the reference to the article for in-depth understanding.

And by surface I mean you should read the article yourself for the full version rather than relying on this book. There may be things one misses while trying to simplify things.

Note: For each chapter, one will post the references in the bottom of the page. Note there will be lots of similarities between the referenced articles and this book, as the concept does not change.

Another note: Some links I will put, others that link to other parts of the book I won't put. One reason is I'm lazy to do so. Another being trying to link to another part of the book encourages you to jump here and there, which I'm trying to discourage here. So, read on, and you'll encounter it some time later.

Certainly you can jump by referring to the table of contents, but that's on your own effort rather than myself making it too easy for you to jump ahead. If you do want to learn a specific topic, feel free to jump ahead!

NEAR Protocol

Cryptocurrencies have their own development platform for developers to write program. The protocol defines how the programs are written. Actually, the protocol defines how to build the system, and hence indirectly defines how programs are written. In NEAR, it's called the NEAR Protocol.

As of writing, NEAR isn't the most dominated marketplace cryptocurrency. Ethereum is. Hence, when we are trying to persuade people to build upon the NEAR Protocol rather than Ethereum's, we want to compare their differences.

The most prominent reason being Ethereum has high gas fee. Due to scalability problem, when more and more people performs transaction in Ethereum, traffic congested, and gas fee escalates. And by expensive it can means a few dollars per transaction, or even more, depending on the traffic.

Furthermore, Ethereum only have limited amount of "transaction per second" (tps). Bitcoin have 3 - 7 tps, Ethereum about 15-25 tps. However, compared to PayPal of 115 tps and Visa of 1,700 tps, this is very little. You can imagine your game gets congested because transaction is queuing up. Some of these transactions might take several minutes to even hours. If you set a threshold and wait for gas fee to reach certain acceptable limit (that is too low from average), you might never even have transaction at all, as gas fee stays above your acceptable limit almost all the time.

For more on transaction speed, check out this article.

And NEAR solves the scalability issue using sharding. This allows the gas fee per transaction to stay few cents per transaction, without scalability issues. The actual gas fee depends on the program, and how well the developers have done their work to reduce the gas fee required to operate their smart contracts.

Read more about Doomslug, the block generation mechanism by NEAR, here.

Sharding

Sharding is the horizontal partition of your database into smaller, more manageable tables. The reason we partition it horizontally because users are store as a single entity horizontally. If you had it vertically partitioned, to fetch a single user data, you need to query several shards to finally gather then total data. However, with horizontal partitioning, just querying one shard fetches the whole user data.

Every single unique account is in one shard, and the accounts in that shard will only transact with the other accounts in the same shard.

Of course, the Nightshade, which is what the sharding technology is based on, also have cross-shard transaction capability.

Also, Nightshade is the consensus mechanism on NEAR. Consensus protocols are used to reach agreement on a single value between multiple participants in the system.

To learn more about Nightshade, you can read its paper here.

Proof of Stake (PoS)

It should be clear that NEAR are running on the Proof-of-Stake (PoS) consensus mechanism; while previous generation like Bitcoin and Ethereum are running on Proof-of-Work (PoW) consensus algorithm. PoS see validators chosen algorithmically to provide security to the platform. PoW requires computer to mine (via mining) costly, difficult, time-consuming equations to produce something that's easy to verify and satisfies certain requirements.

PoW rewards in the mining process: rewards in mining is proportional to the fractional of hash power that you have and hash power requires committing real world resources to the problem (electricity), burned to produce a block. PoS reward in the validation and issuance of new coins, rewards proportional to the fraction of supply you owned. If you own 50% of the supply, you get 50% of the reward. PoS is cleaner as you don't need to burn anything, just show you had a certain fraction of coins.

Check this out for full explanation: https://www.coincarp.com/learn/what-is-the-difference-between-pow-and-pos/

What can you do on the NEAR Ecosystem

Well, you can do lots of stuffs. You can create NFT on Mintbase and sell them. NFT are non-fungible token, which we'll discuss them later on in this chapter. For now, just know that NFT could be music, art, images, or anything you see on the market. Paras is another marketplace you could go to buying/selling NFTs.

How NEAR Blockchain works

Blockchain can safely store transaction records on a peer to peer (p2p) network than storing in a single location. Independent servers around the world, called nodes, make up the network that operates the blockchain.

Decentralization is an important concept in blockchain technology. With decentralization, no control from a single party like the government could stop you from deploying your app. Also, user benefits from the transparency of decentralized apps, with open source code, and how personal information are used.

DeFi

DeFi means Decentralized Finance, which means the utilization of blockchain to create an open-source, permissionless, and decentralized financial system to operate independently of any third party or central governing authority.

In NEAR, there's the Rainbow Bridge to allow bridging from NEAR to Ethereum (and back again). This allow you to have compatibility with Ethereum, the most popular trading cryptocurrency in the market. There might be some support for other cryptocurrencies in the future that doesn't built on Ethereum. You might want to search up on that.

DAO

DAO stands for Decentralized Autonomous Organization. To understand this, imagine the large conglomerate we have today. Let's just use Facebook. Most of the decisions come from the top tiers in Facebook company, passed down in hierarchy their thoughts and how execution should go. In contrast, holders of the governance cryptocurrencies in DAO can voice their concerns on how the future of the company will go. Hence, conglomerate operates in centralization, where you have a few top tiers decide the stuffs; while DAO future depends on their people that participate in DAO governance.

We'll talk more about DAO in the upcoming chapter.

Aurora

Aurora provides Ethereum Level-2 Experience on NEAR Protocol. This allows developers to reach out to additional market (on Ethereum) while taking advantage of NEAR Protocol gas fee remuneration and sharding. The bullet points that frequently explains Aurora's advantages are:

  • The ability to process thousands of transactions per second, 50x increase over Ethereum.
  • Block finality time is 2 seconds, compared to 13 seconds on Ethereum.
  • EVM (Etereum Virtual Machine) can scale horizontally with NEAR's sharding approach.
  • Fees on Aurora are 1000x lower than on Ethereum.
  • Aurora offers uncompromising compatibility with Ethereum over NEAR Protocol.

Learn more about Aurora here.

Looking from the higher perspective

Previously we spoke about NEAR Protocol, but what knows, NEAR Protocol is just one of the few projects developed by a higher level entity, called the NEAR Collective.

According to this article, the NEAR Collective are a group of people (developers, etc) trying to figure out how to build the new internet called Web 3.0.

NEAR Collective is the team that aims to bring the project to life. Each group focus on different things, such as one on NEAR Protocol, others focus on Aurora, etc.

Smart contract

Basically, a smart contract is just... a contract. Just like a usual contract that fulfills certain requirements, a smart contract does the same. Except that it is smart. "Smart" here, one hypothesize, means digital. Nothing too fancy.

Referring to this website, it explains smart contract as a

A self-executing contract with the terms of the agreement between buyer and seller being directly written into lines of code.

References

  • https://learnnear.club/what-is-near-protocol/
  • https://near.org/blog/the-beginners-guide-to-the-near-blockchain/
  • https://learnnear.club/what-is-near-protocol/
  • https://www.whatbitcoindid.com/wbd190-nic-carter

NFT

NFT stands for Non-Fungible Token. Let's understand what it means to be Fungible, and hence non-Fungible.

Meaning of Fungible

Fungible asset is one asset that can be exchanged for another asset. Example: Bitcoin. If you hold two Bitcoin, they're essentially the same value. You can trade one Bitcoin for another Bitcoin.

Non-fungible assets, on the other hand, are unique assets that cannot be traded. These are stuffs that are limited edition. If you draw a unique painting and you only sell 1 copy of it, then that's it. Of course, you can have multiple copies of an NFT on the market. Provided you don't sell it indefinitely (unlimited copies), it's considered an non-fungible.

Every NFT have their unique identifier for identifying them. This is a smart contract being attached to the asset that makes it special. While fungible assets can be exchanged and even divisible into smaller value, NFT aren't divisible. You cannot tear up a painting and sell each pieces individually.

NFTs are digital certificates of ownership for digital assets. Currently we think of "limited edition" in terms of object. And what kinds of things can be limited edition? The first we think is some kind of objects that are sold online can be limited. Yes that is one. But perhaps one of the easiest to think of is tickets. If you want to sell entry ticket for a concert, you could mint them as NFT and sell them. This can replaces the physical ticket that you usually have.

To know more about the top NFT projects, visit the webpage. Note that the website might expire. When I learned, it's "top 3 NEAR NFT Projects", and now it's "top 7". So you might want to search it up in all guides for the equivalent article.

References

  • https://learnnear.club/top-7-near-nft-projects/

DAOs

As mentioned before, DAOs are Decentralized Autonomous Organization. The idea is creating an internet-native business that's collectively owned and managed by its members. No CEO controlling flow of business, no CMO, no CFO, no centralized board of directors. Instead, business logic and execution are baked into smart contracts to ensure streamlined deployment.

DAOs execute certain rules recorded on smart contract and kept on blockchain. Smart contract is like an agreement that hold a DAO together between different parties. It is impossible to change the code without everybody knowing, since the smart contract is transparent. Hence, the only way to change an existing smart contract is via consensus: everyone agrees with the change.

We won't be talking about the difference between traditional hierarchical here. Visit the references link to find out more.

Though, one important point to talk about is: before blockchain, forming a formal group (i.e. companies) requires requesting for a license, which is not necessarily granted permission, to be formed. This process is time consuming, expensive, complex, and might result in restrictive rules once formed.

Hierarchical vs DAOs

DAOsHierarchical
StructureFlat and democratized.Hierarchical
VotingEvery single change in the system requires voting by the membersWhether a system requires voting or not depends on the structure and the rules defined by the system.
IntermediaryVotes are tallied and the outcome implemented without a trusted intermediary.If the voting is allowed, they need to be tallied by an intermediary.
AutomationGovernance-related issues are handled automatically in a decentralized manner.Requires human and centralized handling. Hence they are vulnerable to manipulation.
VisibilityFully public and transparent.Private and mostly opaque.

Membership models in DAO

There are two models:

  • Token based.
  • share-based.

Token-based

Most protocols have a governance token that can be traded in various centralized and decentralized exchanges. One can earn these tokens by participating in consensus algorithm or providing liquidity. Owning these tokens gives you access to voting rights within the protocol. The more you hold, the larger your voice is within the organization. Example: MakerDAO and MKR tokens.

Share-based

More permissioned. Potential members must submit proposals to join the DAO. A participant's share represents their direct voting power. They may exit any time with a proportionate share of the treasury. MolochDAO is an example. You can't buy governance token, and you need certain expertise and capital to join such DAO.

DAO Hack of Ethereum

If you check the market, you'll see Ethereum and Ethereum Classic. They used to be one family. Let's find out the story behind it.

Back in 2016, Ethereum DAO was created, and DAO token holders can propose various projects. If a proposal receives 20% vote or more, they will get the funds to get started.

The DAO also have "split mechanism", where if members aren't happy with the projects whitelisted, they could always split away from the main DAO and create their own "child DAO". This can happens for the minority. Minority no longer have to abide to the majority.

Then a significant code vulnerability was discovered by a hacker, and on 17th June 2016, about $50 million worth of ETH was stolen by the attacker. Most of the community decided to roll back on this incident via a hard fork; while the rest was against this move.

Those that hard fork are called the Ethereum Classic (ETC), while those that move forward are called Ethereum (ETH).

NEAR Protocol and Sputnik DAO

NEAR also have its own DAO, called the Sputnik DAO. Learn more about Sputnik DAO from the references page. We won't go into specific details here.

One important to list about Sputnik DAO is the inspirations it took from DAO's "first wave" of experimentation (i.e. Ethereum and other DAOs that exist before Sputnik).

  • The way the community control governance doesn't scale with the number of people involved, perhaps due to lacking of interest from members in voting; or lack of unifying, driving factor.
  • It's impossible to have completely happy DAO system. Hence, the minority could potentially fork or diverge from the group to follow a new DAO system.
  • A community or organization needs leadership. Hence, there must be dedicated (hardcore) members who are responsible for coordination, and potentially moderation.
  • Even if community is permissionless and pseudonymous, there should be some publicly known members creating the community's core.
  • The more "product" focused a decentralized community is, the more focused the leadership must be.
  • Pure stake-based weighting always fails because most stakeholders are not able to or don't want to be active participants.

References

  • https://learnnear.club/what-are-daos-looking-into-sputnik-dao/

Validators

Validators are responsible for maintaining consensus within the (NEAR) protocol. They need to maintain their server uptime 100% and keep their systems continually updated.

There are a few points to learn about network validators:

  • Every new epoch, NEAR will make choices about whom to become validators, electing them based on their stake.
  • The already elected validators are re-enrolled by automatically re-staking their tokens plus accrued rewards.
  • Potential validators have to have their stake above a dynamically determined level.
  • Validators can strengthen their stake via buying more of the token or borrow the token via stake delegation.
  • The reward received is directly proportional to stake. More stake, more rewards.
  • Validators that misbehaves (e.g. less than 100% uptime, etc) will get warnings before kicked out. In extreme cases, kicking out in the next epoch will occur.

Participants staking with validators

Participants (holder of the tokens) can stake their tokens with validators to earn some passive income by staking their tokens with validators. After every epoch, their tokens and the rewards are re-staked until the participants want to release their tokens.

Note that during staking, their tokens are locked. Unless otherwise specified, tokens still remain inside your wallet (if staking from NEAR wallet) but they aren't visible anymore. You can proof this is true by exploring the staking function that is called by checking NEAR Explorer for your wallet. There are no transfer of tokens from one wallet to another being visible.

Note that, like other investment methods, there are risks of staking. If the validator misbehaves, you may have part of your rewards slashed. Part of your token that are staked are gone, as a punishment to support the misbehavior. Hence, it's a good idea to unstake your token from time to time when you think the reward is large enough that you feel heart hurt if losing it. And it's also very important to do research about the validator on their past behaviors, and only stake with trustworthy validators.

If the validators stop participating (pauses) for the next (several) epoch(s), your earnings will also get pauses. This might be the validator requires maintenance of their server or whatever reasons. Certainly they will come back online, and your rewards will resume earnings. During pausing, you can choose to unstake and your tokens and rewards will get to you.

Validators can charge their participants a fixed percentage of their rewards to stake at their nodes. This is fair share, since participants don't need to run servers to get money, while validators do. Hence, to encourage validators to continue participate, they are allowed to charge fees from their participants, up to a total of 100%. Though, you wouldn't want to stake with a validator that charges 100% fee since you earn nothing.

Epoch

The rewards are given out every epoch. On NEAR, one epoch is about 12 hours, mentioned by sites. However, as of writing, this is not true.

Note participants unstaking their rewards requires waiting for 4 epochs before they can withdraw. During these waiting periods, no earnings from staking are earned. Your locked tokens and their rewards remains remnant waiting for unlocking.

If you run the unlocking twice, the clock will get reset and you'll have to wait for 4 epochs since last unlocking. This unlocking twice means your tokens that are unlocking, perhaps there is a button to unlock that unlocking tokens and you accidentally clicked on it.

If you are staking on separate validators, unlocking are calculated separately based on each tokens being unlocked.

Previously, we mentioned 12 hours per epoch is not true anymore. This is because, 4 full epochs is \( 12 \times 4 = 48 \) hours. However, as of writing, you requires to wait 52 - 65 hours before unstaking.

Actually, one is thinking you might have to wait 4-5 epochs now, and each epoch are 13 hours. One isn't sure about this.

References

  • https://learnnear.club/what-is-near-protocol/

Gas

Making calls to NEAR blockchain that requires computation power will be charged gas fee. These gas fee compensates the people hosting the server (called validators as we seen in the previous section) for their resources provided. This is similar to renting a cloud service, where you pay the cloud service providers for their resources. However, cloud service providers charged monthly after totalling total resources used, while blockchain users get charged immediately.

Though, some users might not want to pay gas fee at the beginning. Example if you developed a decentralized app (dApp) game product, and you want to pay for your user for the beginning, just to give them a head start and let the user decide whether to continue playing the game or not. NEAR allows developers to cover gas costs for users in that situation. These are called prepaid gas, which you can check in the docs (see References).

There are two concepts to keep in mind when thinking in gas:

  • Gas units: transaction fees are not in NEAR token, but gas units. NEAR token is not deterministic (i.e. stable). As of writing, 1 NEAR costs $10.86. During market peak, 1 NEAR was about $20. But gas units is deterministic. The same transaction always cost the same number of gas units.

You should also note that, should the price of NEAR goes up or down a lot, there is a multiplier that will counter against this to ensure that you always pay the same gas price for the transaction. A transaction that cost 2 cents when 1 NEAR = 10 USD won't become 4 cents when 1 NEAR = 20 USD. The multiplier would be halved so that 20 USD times half of multiplier will still give the same gas units, which equals 2 cents. This multiplier is called gas price.

Gas price: The multiplier that gas units are multiplied by to determine how much to charge users. The price is automaticaly recalculated each block depending on network demand, and bottoms out at a price configured by the network, which is 100 million yoctoNEAR.

1 NEAR = \( 10^{24} \) yoctoNEAR (yN).

Learn how to check the gas price by following this link.

Thinking in Gas

NEAR has more-or-less one second block time, accomplished by limiting the amount of gas per block. You can query this value using the protocol_config RPC endpoint and search for max_gas_burnt under limit_config. Let's look at some easy to think numbers:

  • \( 10^{12} \) gas units, or 1 TGas (TeraGas)
  • \( \approx \) 1 millisecond of "compute" time
  • which, at minimum of 100 million yN, equals 0.1 milliNEAR charge.

The above is a rough but useful approximation. Gas units also includes bandwidth and IO time, not just compute. Hence, tweakable parameters allows changing gas price, and the ratio of TGas and milliseconds in the future.

Common actions

Let's give us some starting point values.

OperationTGasfee (mN)fee(Ⓝ)
Create Account0.420.042\( 4.2 \times 10^{-5} \)
Send Funds0.450.045\( 4.5 \times 10^{-5} \)
Stake0.500.050\( 5.0 \times 10^{-5} \)
Add Full Access Key0.420.042\( 4.2 \times 10^{-5} \)
Delete Key0.410.041\( 4.1 \times 10^{-5} \)

More complex actions like function calls, please refer to the docs. In general, there are no mathematics that could calculate that easily, that's why we attached extra gas.

Extra Gas are attached to the function calls and use on "per-basis" for difficult to calculate gas costs function calls. You can attach a certain amount of gas, and see how much it gets burnt. Extra gas that are not used will get refunded, so it's okay to attached much more extra gas than is used.

References

  • https://learnnear.club/what-is-near-protocol/
  • https://docs.near.org/docs/concepts/gas

Rainbow Bridge

Rainbow Bridge provides a permissionless, trustless bridge to Ethereum ecosystem.

Physical transfer of tokens isn't possible. Let's say DAI (an Ethereum-based coin). If you want to take DAI out of circulation on Ethereum and put it to NEAR, you need to make sure the global circulation doesn't change. This is how they do it:

  1. Tell Ethereum network to transfer DAI somewhere else.
  2. Ethereum network locks DAI in a vault using smart contract, so they're taken out of circulation.
  3. Once it's certain it's locked, tell NEAR to create an equal amount of DAI in new circulation.
  4. NEAR doesn't trust the request, so it asks for proof that it's locked on Ethereum.
  5. The proof is provided to NEAR.
  6. NEAR independently verifies the proof, then creates equivalent DAI to use on NEAR after successful verification.

The procedure is reversed to move DAI from NEAR to Ethereum.

Let's see how that happens with the help of Rainbow Bridge.

The Actors

Rainbow Bridge UI interacts with the user.

The LiteNode is like a blockchain node, except it only stores block headers, dramatically reducing storage space needed. They are smart contracts, one deployed on Ethereum network (storing NEAR Block Headers), and another deployed on NEAR network (storing Ethereum Block Headers). It's also called the "light client".

Relayers are scripts running on traditional servers that periodically read blocks from one blockchain, and communicate them to LiteNode running on the other, keeing LiteNodes up-to-date. Since this requires calculation, it charges gas fee. Each relayer update will update the LiteNode contract on NEAR, but Ethereum's update only every 12-16 hours (due to expensive gas fee).

Connectors are smart contracts responsible for all the logic associated with cross-chain management of a given asset type. They exist in pairs, one on Ethereum, another on NEAR. There is a pair of ETH Connectors responsible for transferring ETH between 2 networks. And another "ERC-20 Connector" pair responsible for transferring ERC-20 tokens. Any type of connector can be written by software writers, to bridge any transfer across the Rainbow Bridge, not just Ethereum and NEAR.

Rainbow Bridge underlying transfer

How does the Rainbow Bridge transfer? Let's say we have 20 DAI.

  1. Using Rainbow Bridge UI, start the transfer of 20 DAI.
  2. Confirm the first of two transaction in MetaMask, and Rainbow Bridge communicates with ERC-20 Connector on Ethereum, transferring and locking 20 DAI in its vault from circulation.
  3. Based on header data in our transaction block, the Rainbow Bridge UI calculates a cryptographic "proof" for locking DAI.
  4. Wait for Relayer to send about 20 Ethereum block headers to LiteNode running on NEAR. This is for security purposes, the same way your crypto exchange makes you wait for confirmations before using deposited funds.
  5. Then, Rainbow Bridge UI takes us to step 2: asking ERC-20 Connector on NEAR to create 20 new DAI for us on NEAR network.
  6. We provide the cryptographic proof generated earlier upon request.
  7. The ERC-20 Connector on NEAR then lookup our Ethereum block header in LiteNode running on NEAR, and make its own independent calculation of cryptographic proof.
  8. If provided proof matches the proof calculated, we know 20 DAI is safely locked away on Etereum, and it's the correct person locking it up, hence we proceed to create (mint) 20 new DAI on NEAR and delivers them to their wallet.

For transferring back, DAI on NEAR are burned (process called "burning"), providing proof of burning to Connector on Ethereum. This will release DAI from vault and sends to our wallet.

Other information

  • There is a delay moving tokens from NEAR to Ethereum, as the LiteNode only performs calculation once every 12-16 hours. Approaches on reducing the delay are proposed.
  • For storage space reason, LiteNode "prunes" (deletes) blocks on NEAR older than 2 weeks. If you transfer Ethereum to NEAR and in between wait for 2 weeks (between step 1 and step 2), you won't be able to complete your transfer, as proof is deleted.
  • NEAR block header design allows computing the history of past blocks for quite a long period, with a single block header. So in theory, the LiteNode on Ethereum only needs a single NEAR block. However, we don't do that as computation on Ethereum is super expensive. Gas costs to perform pruning is a waste of resources.
  • Aurora (we'll speak more about this later) and Rainbow Bridge are created by the same team.
  • Aurora team is working on "auto-finalization" for Rainbow Bridge, so you no longer need to manually initialize step 2 of these transfers. Hence, if you leave for 2 weeks, your transfer proceeds automatically without your intervention.
  • Aurora Bridge has the same concept as Rainbow Bridge.
  • UI and UX of Aurora Bridge differs from Rainbow Bridge UI.

References

  • https://learnnear.club/how-the-near-rainbow-bridge-works/

Aurora

Aurora is the platform which provides Ethereum Layer-2 Experience on NEAR Protocol. Aurora allows NEAR developer to extend their dApps to Ethereum marketplace, while taking advantage of cheap gas fee by NEAR, and other advantages.

Aurora are made of two components:

  • Aurora Engine allowing seamless deployment of Solidity and Vyper smart contracts, and
  • Aurora Bridge (essentially a Rainbow Bridge specialized for this task).

Aurora enhancements compared to Ethereum:

  • Fees are up to 1000x lower than Ethereum's.
  • Thousands of transactions per second (tps), which is 50x more than Ethereum 1.0.
  • Aurora transaction finality is 2 seconds (same as NEAR) compared to Ethereum's 13s; significantly reducing risk of frontrunning attacks.
  • Ecosystem growth on Aurora is future-proof: sharding approach provides horizontal EVM scaling, with asynchronous communication between multiple Aurora shards.
  • Greener option for ethereum user: PoS than PoW.

Aurora is implemented as a smart contract on the NEAR blockchain. It implements two main interfaces: Execution and Token.

  • Execution interface allows users to send ordinary Ethereum transactions; they get decoded, verified, and executed in the EVM runtime. Some operations may be moved to NEAR Protocol level (and thus become precompiles) in case a smart contract doesn't deliver the target performance.
  • Token: permissionless token bridging with Rainbow Bridge technology.

Some discussion proposes transferring ETH over the Aurora Bridge to NEAR Protocol, such that users can deposit and withdraw ETH to NEAR. The outcome is: NEAR EVM runtime will use bridged ETH (nETH) as a base token.

Aurora runs on top of NEAR, so NEAR gas is the real measure of computational work. However, for compatibility with Ethereum, users can pay their transactions with ether (ETH). Aurora infrastructure includes relayers to encapsulate ordinary EVN transactions into NEAR transactions, submit them on-chain, and return the transaction result.

Look at the doc's FAQ for more info.

Resources

  • https://near.org/blog/aurora-launches-near/
  • https://doc.aurora.dev/
  • https://doc.aurora.dev/compat/gas

Appchain

A dApp (decentralized Application) is a web app with at least a portion of its backend residing on a blockchain. Its backend could either be smart contracts hosted on blockchain app, or a dApp could live on its own dedicated blockchain.

A dApp that has its own blockchain is called an appchain (application-specific blockchain). Unlike dApps, appchains allow developers to customize their application in terms of governance structure, economic design, and even its underlying consensus algorithm.

Appchain also gives developers dedicated transaction processing capacity, meaning an app on an appchain doesn't have to compete with other apps for transaction processing acpacity on a network.

While thousands of different apps might share a standard set of configurations on a generic smart contract platform, each appchain in PoS setting could easily achieve 1K+ tps throughput and fast finality -- all dedicated to just one app.

Unlike smart contracts, appchains can evolve quickly with legitimacy. Each appchain is a self-governed economy with code-defined explicit processes to reach agreements on protocol upgrades. Using Substrate (the library to program Polkadot), the primary function of on-chain governance is ready to use. Any cryptonetwork can mirror the governance process of another by copy-pasted code. Blockchain governance itself could evolve like open-source software.

Extra Note

This article is originally taken from one discussing the Octopus Network, a DeFi that operates on NEAR Protocol. Have a look at the original article (link in references).

References

  • https://learnnear.club/what-is-octopus-network/

Smart Contract Programming

One remembers the book written by Michael Hartl: Learn Enough Rails to be Dangerous. It's a great tutorial on how he teaches Rails. One can't say enough that one is copying the style to teach you guys on NEAR. So, here the first paragraph on this chapter is to credit his teachings, and style of approaches. You can check out his Learn Enough to be Dangerous website if there are anything you would like to learn about.

What we are going to do, is to go through several examples and let you see how the code are written, based on smart contracts already written and available in GitHub. The thinking goes like this: if you read and write enough smart contract, you can slowly pick up other smart contract on the way and learn it yourself, without having somebody explaining to you. Ultimately, the target of this course if you become a NEAR developer, and as a developer, reading and writing smart contract (especially reading) are usual plates of food. If you're already a developer, you already have the skill. One hopes to kickstart your journey, making it easier for you to read and understand code in the future.

Also, the code written will be somewhat different from the convention of most developers. Particularly, when one writes code, one tends to optimize for readability and understandability, so there may exist variables that are longer than usual (so it could be easily understandable), names that aren't following the convention (when convention doesn't explain much), for example. Nevertheless, one would put brackets around explaining what the original code uses so it eases you to read the code in the future).

Next is to choose the programming language. The frontend doesn't matter. One is a backend developer so any frontend requires one to learn, and we'll see which one to use as we go. For the backend, we'll use Rust (as opposed to AssemblyScript) (for personal reason which Rust's convention makes me happy while TypeScript not).

Though one have this weird fancy inconsistent tab space used, though, that might annoy you. In fact, one like to have 4 spaces for the first tab from the side, but subsequent tabs one like to have only 2 tabs, so you might find it weird to have this kind of tabbing. Let me show you an example (the cod e doesn't make much sense, btw).

fn some_fn():
    // four spacing at the beginning. 

    let name = "Rust";
    assert!(
      // two spacing thereafter
      name == "Rust"
    );

And it depends on how clutter the code is, one might use one spaces or two spaces between the functions or implementations for example. Sometimes it makes things easier to see with separators, so one might put some separators as well.

fn one_fn(): 
    // --snip--

// ===============Another section=============== //
fn another_fn():
    // --snip--

But it's not necessary always true that one have a convention. Basically, whatever that makes thing looks beautiful AND makes me happy is the way to go. :)

And there are some useful tips and tricks with Rust, which you can find here: NEAR SDK IO which teaches a lot on how to write smart contract in Rust, and what are some common stuffs that can be changed despite the convention to optimize for contract size, etc. The page it links is just one of the page, navigate to the other pages yourself (note if you have the page open half-screen, the table of content will appear on the bottom right of your screen compared to top left when you open the page semi-fullscreen).

Notes

Note that one write in this way as it makes one happy. You DO NOT have to follow the style of one's writing. If you have your own programming style, you're welcome to adapt to how you usually do things, it makes you happy anyways!

Non Fungible Token Tutorial

This tutorial is based on the ZERO to HERO series of NFT tutorial. We are almost copying what they have there, except for some changes and added additionals:

  • Make the code naming easier to understand, in some cases.
  • Explanation on the logic whenever it got confusing.
  • (Note: still need to think whether to implement this) We will also build a front-end for this, to make the example end-to-end.
  • But at the same time, show near-cli and their similarities? For this example or for other examples?

You might want to follow the original code. In fact, one suggests that as one might miss out stuffs (or deliberately leave out stuffs) that's available in the original site but not available here. Nevertheless, one tries to make this as standalone as possible so you don't have to refer back and forth.

Also, one isn't a fan of conclusions so one will skip recalling what we done, against the norm. To have a recall, visit the original references, or just read through the page again to recall.

References

To be added


#![allow(unused)]
fn main() {
let mut token = if let Some(token) = self.tokens_by_id.get(&token_id) {
  if token.owner_id != receiver_id {  // receiver_id is the receiver. 
    refund_approved_account_ids(owner_id, &approved_account_ids);
    return true;  
  }
  token
} else {  // no token object, it was burned. 
  refund_approved_account_ids(owner_id, &approved_account_ids);
  return true;
};
}

Cannot move out refund_approved_account_ids as it leads to different results.

On ext_contract

A.K.A. Callbacks.

One used to have some problem with this, because one was trying to find a trait but cannot find it. Here, we shall discuss how to use ext_contract and "why some code cannot be found from import". We discuss about a code and see how the logic goes. And finally, we discuss some confusing elements at the final part of this page.

This applies if you haven't really get deep into Rust (like myself). That is, you don't really know what the code is trying to do.

Let's look at this from the resource example:

use near_sdk::ext_contract;

#[ext_contract(ext_calculator)]
trait Calculator {
  fn mult(&self, a: U64, b: U64) -> U128;

  fn sum(&self, a: U128, b: U128) -> U128;
}

At first, one thought the line #[ext_contract(ext_calculator)] is a wrapper that pass in the trait to some wrapper.

Well, ext_contract is indeed a wrapper, but ext_calculator isn't. ext_calculator is, after the trait is being wrapped by ext_contract, you can call the trait with ext_calculator. Hence, if you want to find ext_calculator in near-sdk-rs or other imports but cannot find it, this is the reason.

And ext_contract just adds a few more variables to whatever you defined. These are:

  • receiver_id (of type &AccountId): The account calling the contract.
  • attached_deposit (of type Balance): did you deposit some NEAR to the cross contract call? Usually will be NO_DEPOSIT. And when you have deposit, the function needs to be #[payable] or it'll panic.
  • attached_gas (of type Gas): attached gas to pay the gas fee.

Conclusion

So whenever you call the function, make sure to add the extra three parameters in. Let's make some example:

const CONTRACT_ACCOUNT_WE_ARE_CALLING: &str = "calc.near";
const NO_DEPOSIT: Balance = 0;
const BASE_GAS: Gas = 5_000_000_000_000;

#[near_bindgen]
impl Contract {
  pub fn sum_a_b(&mut self, a: U128, b: U128) -> Promise {
    let deploy_acc: AccountId = CONTRACT_ACCOUNT_WE_ARE_CALLING.to_string();

    ext_calculator::sum(
      a,
      b,
      &deploy_acc,
      NO_DEPOSIT,
      BASE_GAS
    )
  }
}

Similarly for multiplication

pub fn mult_a_b(&mut self, a: U64, b: U64) -> Promise {
  let deploy_acc: AccountId = CONTRACT_ACCOUNT_WE_ARE_CALLING.to_string();

  ext_calculator::mult(
    a, 
    b,
    // extra params
    &deploy_acc,
    NO_DEPOSIT,
    BASE_GAS
  )
}

An example, a problem

Then let's look at a problem that's hard to understand.

fn nft_resolve_transfer(
        &mut self,
        owner_id: AccountId,
        receiver_id: AccountId,
        token_id: TokenId,
    ) -> bool {
        // whether receiver returns token back to sender, based on 
        // `nft_on_transfer` call result. 

        if let PromiseResult::Successful(value) = env::promise_result(0) {
          if let Ok(return_token) = near_sdk::serde_json::from_slice::<bool>(&value) {
            // don't need to return token, simply return true. 
            // everything went fine. 
            if !return_token {
              return true;
            }
          }
        }

        // get token object if got some token object
        let mut token = if let Some(token) = self.tokens_by_id.get(&token_id) {
          if token.owner_id != receiver_id {  // receiver_id is the receiver. 
            return true;  
          }
          token
        } else {  // no token object, it was burned. 
          return true;
        };

    // --snip--

Now the confusing part is this:

if token.owner_id != receiver_id

Wait, if we have token equals receiver, that means receiver already receive it, then why we need to return to the owner?

Finally one understand, the logic isn't with this line, but with the previous block of code:

if let PromiseResult::Successful(value) = env::promise_result(0) {

We had already pass this stage before, so that means if we gone through and it hadn't return true, the transaction wasn't successful. We want to resolve it by transferring the token back to the owner. Then it makes sense. Our token.owner_id is set to receiver_id, it needs to be returned to the previous_owner. That's why we want to set the value back, to allow the token to be transfer back to owner.

Alternatively, the token can be burnt. So, one guess the token is lost forever in this case? One isn't sure, though.

Also, this means that if we don't have the first code block of confirming that the PromiseResult is successful or not, then the code token.owner_id != receiver_id per se doesn't make sense. If you don't have the first code block checking for successful promise, we would assume to return true if token.owner_id == receiver_id, that means the transfer is "assumed to be successful". In fact, here, it's not "assumed to be not successful", so we flip the double equal sign as well.

Some confusing element

Note the calculator example uses receiver_id and it says calculator is deployed on calc.near. This has nothing wrong per se. This is because the cross-contract call is "to itself", meaning owner_id = receiver_id = "calc.near". So when both are the same, it doesn't really matter who's the caller and who's the host.

But of course, to the readers, owner_id and receiver_id isn't the best name to use. One suggests you use the following name when writing your code so as to not confuse your readers (or requires elaborated explanation at different phase):


#![allow(unused)]
fn main() {
contract_caller  // whoever calling the contract, hosted elsewhere

contract_host  // the account hosting the contract
}

These are easier to understand when someone reads your code. (Of course you can always use elaborated explanations. Make sure to put it near where you call it, or the reader won't be able to find out about it, or at least difficult to find out about it. The choice is yours).

Resource

"NEAR Place" Tutorial

This is the NEAR Place code. This contract has the same principles as Berry Club game, except it's more simpler than that.

We have a platform where we can draw on the front end, and for the pixels you draw, you earn. You have to first pay to buy the quota to draw, each pixels cost the same. Based on how much you draw, you earn.

When someone else comes in and draw on top of yours, your pixels got overridden, and you earn less after that, since you occupy less pixels on the board. To earn back, you need to re-draw to overwrite their changes, so you're the owner of those pixels again.

You can think this as a land, and having multiple empires warring against each other for the land. Each pixels represent a land. Perhaps you send some army to occupy a land (which requires you to pay for the food of the army, and other costs). Now that land is yours, and there may be buildings on top of them which you can rent out to others. Or perhaps you charge tax for the people that lives there. (And hence, each pixel you owned earn you some money).

Then, someone may come in and occupy your land, the land is no longer yours, so the earnings/tax goes to the owner whoever occupied your land before. And if you want to earn the tax, you need to fight it back. The process of fighting it back, you still have to pay for the food and other expenses of your soldiers (money to draw pixels).

So right, this is an analogy of how it seems to work.

We shall look into the code of how it worked out. This code also have a link to the front end so we shall also look at how the communication goes with the smart contract.

References

  • https://github.com/near-examples/place

Account

We'll start by looking at account.rs

The first things we see are the costs:

pub const PIXEL_COST: Balance = 1_000_000_000_000_000_000;
pub const DEFAULT_BALANCE: Balance = 100 * PIXEL_COST;
pub const REWARD_PER_PIXEL_PER_NANOSEC: Balance = PIXEL_COST / (24 * 60 * 60 * 1_000_000_000);
  • The PIXEL_COST is how much it cost to draw a pixel
  • DEFAULT_BALANCE is how much money do you have at the beginning of the game. Berry Club will require you to buy some cucumbers, but this Near Place is give some fake money to you (on testnet) to play with.
  • REWARD_PER_PIXEL_PER_NANOSEC is explaining itself: how much tax is paid to you for the pixels you occupied. The current reward is 1 pixel per day per pixel (see the maths up there).

Then next block

pub type AccountIndex = u32;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct Account {
  pub account_id: AccountId,
  pub account_index: AccountIndex,
  pub balance: u128,
  pub num_pixels: u16,
  pub last_claim_timestamp: u64,
}

These define the Account struct for us. What's worth noting here is, we also decide the types to use. For example, developers usually uses u32 for those defaults unless they need to pump it up. But if you see in the listing above, one can decide to use u16 as well if one think that one will not need that much space.

So that was what one did for num_pixels. If one only have less than 65536 pixels, then one can use u16, however if one need more than that, then one need to up it to u32 or even u64 depending on use cases. So this really depends on your design.

Though why one may want to do this. One hypothesize it might reduce the storage of the output wasm file, maybe. Whether or not it does requires further experimentation.

Looking deeper into the code, these are the things you need for the account. Though one isn't sure why do we need AccountIndex for though, we'll see it in the future and come back to this.

And last_claim_timestamp is really, when was the reward last claimed. So this was originally called claim_timestamp but one thought it might not be clear enough, hence changed the name.

Of course, we have a struct, we would need an implementation, that's the usual way it goes. A struct, and an implementation, (and optionally Default implementation).

impl Account {
    pub fn new(account_id: AccountId, account_index: AccountIndex) -> Self {
      Self {
        account_id,
        account_index,
        balance: DEFAULT_BALANCE.into(),
        num_pixels: 0,
        last_claim_timestamp: env::block_timestamp(),
      }
    }

    pub fn touch(&mut self) {
      let block_timestamp = env::block_timestamp(),
      let time_diff = block_timestamp - self.last_claim_timestamp;
      self.balance += Balance::from(self.num_pixels + 1)
          * Balance::from(time_diff)
          * REWARD_PER_PIXEL_PER_NANOSEC;

      self.last_claim_timestamp = block_timestamp;
    }

    pub fn charge(&mut self, num_pixels: u16) {
      let cost = Balance::from(num_pixels) * PIXEL_COST;
      assert!(
        self.balance >= cost,
        "Not enough balance to draw pixels"
      );
      self.balance -= cost;
    }
}

Usually when we have arguments that we would like to pass in default values (like num_pixels always starts with 0 at the beginning), and there are some other arguments that requires user interaction to pass in certain values (like account_id), we will have a new function. The new function is not that new in smart contract context. If you come from other programming background, you most probably wrote this kind of function yourself in OOP classes. In Python, this is called "class method".

And env::block_timestamp() just uses the block height. If you want to understand more about time, check the Appendix "Bitcoin is Time" in Other Notable Resources for more details about how Bitcoin time is created (which is also used in NEAR).

And we have the touch function. This means, now that we want to claim our rewards. So we know how many REWARD_PER_PIXEL_PER_NANOSEC, and we'll be using that together with the time difference since we last claimed (or since we made our account), to calculate how much rewards we are eligible to claim. This number is put into self.balance. Though one don't know why do we need the + 1 for the num_pixels though.

And then, we reset the self.last_claim_timestamp to the current block_timestamp.

Finally, is charge. Just by looking at the assertion "Not enough balance to draw pixels" we can understand this function is used to draw pixels, and the name of the function suggests this function will charge you a certain cost for drawing the pixels. The code is fairly easy to understand if you read it like storybook.

Note that aside from Number 4 of this site to reduce contract size, if you're using near-sdk-rs version =4.0.0-pre.2 onwards, there's something more lightweight than assert! called require!

use near_sdk::{require};

impl Account {
    // --snip--

    pub fn charge(&mut self, num_pixels: u16) {
      // --snip--
      require!(
        self.balance >= cost,
        "Not enough balance to draw pixels");
      )
      // --snip--
    }
}

Of course, there's some difference in that you need to use format! if you want to print "{}" something that requires variable insertion; while in assert_eq! macro for example you don't need to do that.

Next, we have Place. The struct of Place is in lib.rs rather than here, but one will show you here as well. The explanation will come later, though. Here are just to show you what arguments are being passed in.

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Place {
    pub account_indices: UnorderedMap<AccountId, u32>,
    pub board: board::PixelBoard,
    pub accounts: Vector<Account>,
}

Then now we know the arguments, we shall move on to impl Place without further explanations, for now.

impl Place {
    pub fn get_account_by_id(&self, account_id: AccountId) -> Account {
      let account_index = self
          .account_indices
          .get(&account_id)
          .unwrap_or(self.accounts.len() as u32);

      self.accounts
          .get(u64::from(account_index))
          .map(|mut account| {
            account.touch();
            account
          })
          .unwrap_or_else(|| Account::new(account_id, account_index))
    }

    pub fn get_account_by_index(&self, account_index: AccountIndex) -> Option<Account> {
      self.accounts
          .get(u64::from(account_index))
          .map(|mut account| {
            account.touch();
            account
          })
    }

    pub fn save_account(&mut self, account: &Account) {
      if u64::from(account.account_index) >= self.accounts.len() {
        // it's a newly created account. Save it. 
        self.account_indices
            .insert(&account.account_id, &account.account_index);
        self.accounts.push(account);
      } else {
        // replace to modify state? ONE ISN'T SURE. 
        self.accounts
            .replace(u64::from(account.account_index), account);
      }
    }
}

The first function we look at is get_account_by_id. The name is self-explanatory, but let us discuss a little bit about the code.

Assuming we have a place where we all store the account_index called self.account_indices. This self.account_indices is a Map (also called a dictionary in Python, whatever it is, it's a Key-Value pair storage). So, we can use the .get() method, passing in the "Key", and it'll return the "Value". If we cannot find it, the function panics, then we'll create a new one for it. For the account index, since it is created incrementally, we'll use the final value as the newest account_index. We don't need to + 1 as counting starts from 0 (try imagine the logic yourself by writing it on paper or use a dynamic language programming like Python or Ruby playing around with it to see how it works in real time).

Then we want to return the account that matches the account_id, as the function promises. So, inside the self.accounts, search for the corresponding account, and now we touch the account as we want to change its state, so we need to map it and touch it before returning. If we cannot find the account, we create a new one.

We could choose not to get_account_by_id. We could instead get_account_by_index too. Here, it's a simplification of the previous function; as the previous function did two things: use id to find the index before finding the account. If we already know the index, we don't need the first block of code.

We don't do unwrap_or_else because index is something more "internal" than id. You can have an id passed in, and if it don't exist, you create an account, just like signing up for an account on a website when you first visit it. But the index is more "internal", which we expect that when we use it, it'll be for something internal, and creation of a new Account is not required. So, nobody will use an index for signup. We assume we assign an index to an id after signing up, then the index could be used internally. If we cannot find it, most probably we have some bug in the code rather than requiring to create a new Account, like forgot to delete the index after a user requests Account to be deleted, or we code the logic wrongly hence given the wrong index.

But if you check the Berry Club contract, we actually see Evgeny decided to return Option<Account> later on during the development. So that's another change; perhaps saying that some assumptions are being changed and perhaps it's more optimized that way.

Then if we modify the state, we also need something to save_account. This could be a creation of new account, or perhaps a modification of state to an existing account, whatever it is.

And we have the #[near_bindgen] implementation. For more information about what #[near_bindgen] did, you can find it here.

In brief:

The #[near_bindgen] macro is used on a struct and the function implementations to generate the necessary code to be a valid NEAR contract and expose the intended functions to be able to be called externally. This includes the generation of necessary boilerplate to expose the functions.

#[near_bindgen]
impl Place {
    pub fn get_pixel_cost(&self) -> U128 {
      PIXEL_COST.into()
    }

    pub fn get_account_balance(&self, account_id: ValidAccountId) -> U128 {
      self.get_account_by_id(account_id.into()).balance().into()
    }

    pub fn get_account_num_pixels(&self, account_id: ValidAccountId) -> u16 {
      self.get_account_by_id(account_id.into()).num_pixels
    }

    pub fn get_account_id_by_index(&self, account_index: AccountIndex) -> Option<AccountId> {
      self.accounts
          .get(u64::from(account_index))
          .map(|account| account.account_id)
    }
}

We have four functions, get_pixel_cost, get_account_balance, get_account_num_pixels and get_account_id_by_index. All of these are view functions, and we can easily understand them by their names, so one won't go deeper into it. The logic implementation tries to change the value into something that can be printed out; example perhaps originally it doesn't implement the std::fmt::Display, but calling .into() will change it to a type that does. So that's about it.

Next, we'll take a look at the board, in board.rs.

References

Board

As the name suggests, this smart contract relates to the Board where we can draw stuffs, so the implementation is whatever that the board logic has.

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

As usual, we use u16 instead of u32. And given that \( 50 \times 50 = 2500 < 65536 \) where \( 2^{16} = 65536 \) the max number for u16, hence we could use u16 here. If we have more pixels, we might prefer to use u32. Of course, for general implementation, you could just use u32 without thinking much about it, it's perfectly fine. One just want to show you that it still works with u16, if however it does optimize performance in any way.

Next, we look at the implementation of each Pixel:

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

We first define what to go into each Pixel. So each Pixel will have a color, and who owns the Pixel. Because it's drawing, of course we'll have a variety of color available for each pixel, or we won't know what is drawn. And color just makes the Pixel more beautiful; it doesn't affect its price, nor the pricing changes according to what color to draw. That could be implemented if required, but not in this game.

Then we have PixelLine.

use crate::*;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelLine(pub Vec<Pixel>);

impl Default for PixelLine {
    fn default() -> Self {
      Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

Now this concept of PixelLine is very useful. In general, they're called Lines, which you can read more about it here.

Lines are something one never thought before so clearly until reading this. One can't remember where one read it (and can't even find the reference), but someone said that you could think of it as "levels" in game. Example you might play a game that passes levels, level 1, 2, 3, etc. The "lines" concept could be imagine as that;

Here our lines are just the "rows" in the board. Line 1 = row 1, line 2 = row 2, etc. Each row contains the metadata of the Pixel that contains within it. This allows easier coordination and handling of data. And since PixelLine is a vector of Pixel, we don't need to alternatively define the coordinates somewhere, provided this is 2D.

So, one isn't really sure why this implementation, but one hypothesize it saves data. Example: a LookupMap containing row and column stored somewhere, or perhaps each pixel changes it's struct to become:

pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
    pub coordinate: (u16, u16),  // row, column
}

might be less efficient? You are welcome to try to change the implementation and play with it, to see which is more optimized for example.

Then we have the board, here it's PixelBoard. We'll skip the struct definintion for now and talk about it later, as this is important. Let's first talk about SetPixelRequest. This function SetPixelRequest is called by the user to "rent" the particular pixel. The user can define the color of the pixel he want to set after renting, so that is required in the struct. Otherwise, the coordinates are also required.

use near_sdk::serde::{Deserialize, Serialize}

pub const BOARD_WIDTH: u32 = 50;
pub const BOARD_HEIGHT: u32 = 50;

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct SetPixelRequest {
    pub x: u16,
    pub y: u16,
    pub color: u32,
}

impl SetPixelRequest {
  pub fn assert_valid(&self) {
    assert!(self.x < BOARD_WIDTH, "X is out of bounds");
    assert!(self.y < BOARD_HEIGHT, "Y is out of bounds");
    assert!(self.color <= 0xffffff, "Color is out of bounds");
  }
}

Then let's come back to PixelBoard with its implementation:

use crate::*;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelLine(pub Vec<Pixel>);

impl Default for PixelLine {
    fn default() -> Self {
      Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct SetPixelRequest {
    pub x: u32,
    pub y: u32,
    pub color: u32,
}

impl SetPixelRequest {
    pub fn assert_valid(&self) {
        assert!(self.x < BOARD_WIDTH, "X is out of bounds");
        assert!(self.x < BOARD_HEIGHT, "Y is out of bounds");
        assert!(self.color <= 0xffffff, "Color is out of bounds");
    }
}

// We rearranged a bit by moving PixelBoard down here, 
// supposedly it's defined before SetPixelRequest. 
#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

impl PixelBoard {
    pub fn new() -> Self {
      let mut board = Self {
        lines: Vector::new(b"p".to_vec()),
        line_versions: vec![0; BOARD_HEIGHT as usize],
      };

      let default_line = PixelLine::default();
      for _ in 0..BOARD_HEIGHT {
        board.lines.push(&default_line);
      }
      board
    }

    pub fn get_line(&self, index: u32) -> PixelLine {
      self.lines.get(u64::from(index)).unwrap()
    }

    /// returns the list of old owner IDs for the replaced pixels
    pub fn set_pixels(
      &mut self,
      new_owner_id: u32,
      pixels: &[SetPixelRequest],
    ) -> HashMap<AccountIndex, u32> {
      let mut lines = HashMap::new();
      let mut old_owners = HashMap::new();

      for request in pixels {
        request.assert_valid();
        let line = lines
            .entry(request.y)
            .or_insert_with(|| self.lines.get(u64::from(request.y)).unwrap())
        let old_owner = line.0[request.x as usize].owner_id;
        line.0[request.x as usize] = Pixel {
          owner_id: new_owner_id,
          color: request.color
        };
        *old_owners.entry(old_owner).or_default() += 1;
      }

      for (i, line) in lines {
        self.save_line(i, &line);
      }

      old_owners
    }

    fn save_line(&mut self, index: u32, line: &PixelLine) {
      self.lines.replace(u64::from(index), line);
      self.line_versions[index as usize] += 1;
    }
}

We have two arguments: lines and line_versions. lines is easy to understand, just the collection of PixelLines, stored as a vector. All existing PixelLines add up to a PixelBoard.

Then there's line_versions. We see this is being used in the save_line function. Checking it, this means that each PixelLine has a version number. Everytime that particular PixelLine has some pixels overridden, the line_versions increment by 1. One isn't quite sure why this is required, though. Perhaps we'll know when we read lib.rs, but based on board.rs, there isn't much information except one described above. The line passed in is the overwritten PixelLine. The line is changed within set_pixels.

We also have the implementation for Place on the board, aside from the implementation in lib.rs. This contains specific implementations that consult the board.

use crate::*;

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};

pub const BOARD_WIDTH: u16 = 50;
pub const BOARD_HEIGHT: u16 = 50;
pub const TOTAL_NUM_PIXELS: u16 = BOARD_WIDTH * BOARD_HEIGHT;

#[derive(BorshDeserialize, BorshSerialize, Copy, Clone)]
pub struct Pixel {
    pub color: u32,
    pub owner_id: AccountIndex,
}

impl Default for Pixel {
    fn default() -> Self {
      Self {
        color: 0xffffff,
        owner_id: 0,
      }
    }
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelLine(pub Vec<Pixel>);

impl Default for PixelLine {
    fn default() -> Self {
      Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct SetPixelRequest {
    pub x: u32,
    pub y: u32,
    pub color: u32,
}

impl SetPixelRequest {
    pub fn assert_valid(&self) {
        assert!(self.x < BOARD_WIDTH, "X is out of bounds");
        assert!(self.x < BOARD_HEIGHT, "Y is out of bounds");
        assert!(self.color <= 0xffffff, "Color is out of bounds");
    }
}


#[derive(BorshDeserialize, BorshSerialize)]
pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

impl PixelBoard {
    pub fn new() -> Self {
      let mut board = Self {
        lines: Vector::new(b"p".to_vec()),
        line_versions: vec![0; BOARD_HEIGHT as usize],
      };

      let default_line = PixelLine::default();
      for _ in 0..BOARD_HEIGHT {
        board.lines.push(&default_line);
      }
      board
    }

    pub fn get_line(&self, index: u32) -> PixelLine {
      self.lines.get(u64::from(index)).unwrap()
    }

    /// returns the list of old owner IDs for the replaced pixels
    pub fn set_pixels(
      &mut self,
      new_owner_id: u32,
      pixels: &[SetPixelRequest],
    ) -> HashMap<AccountIndex, u32> {
      let mut lines = HashMap::new();
      let mut old_owners = HashMap::new();

      for request in pixels {
        request.assert_valid();
        let line = lines
            .entry(request.y)
            .or_insert_with(|| self.lines.get(u64::from(request.y)).unwrap())
        let old_owner = line.0[request.x as usize].owner_id;
        line.0[request.x as usize] = Pixel {
          owner_id: new_owner_id,
          color: request.color
        };
        *old_owners.entry(old_owner).or_default() += 1;
      }

      for (i, line) in lines {
        self.save_line(i, &line);
      }

      old_owners
    }

    fn save_line(&mut self, index: u32, line: &PixelLine) {
      self.lines.replace(u64::from(index), line);
      self.line_versions[index as usize] += 1;
    }
}

#[near_bindgen]
impl Place {
    pub fn get_lines(&self, lines: Vec<u32>) -> Vec<Base64VecU8> {
      lines
          .into_iter()
          .map(|i| {
            let line = self.board.get_line(i);
            line.try_to_vec().unwrap().into()
          })
          .collect()
    }

    pub fn get_line_versions(&self) -> Vec<u32> {
      self.board.line_versions.clone()
    }
}

These are view functions to get_lines and get_line_versions.

References

Main Library

This is the lib.rs file. We will skip some of the explanations already available in this website demonstrating the contract basics and focus more on the specifics.

Recall the Place that we define before. This struct defines the "place" where the board is held, and the participants (accounts) which participate in this game.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{UnorderedMap, Vector};
use near_sdk::{env, near_bindgen, AccountId, Balance, PanicOnDefault};

pub mod board;
pub use crate::board::*;

#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc<'_> = near_sdk::wee_alloc::WeeAlloc::INIT;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Place {
    pub account_indices: UnorderedMap<AccountId, u32>,
    pub board: board::PixelBoard,
    pub accounts: Vector<Account>,
}

Let's get into the implementation.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{UnorderedMap, Vector};
use near_sdk::{env, near_bindgen, AccountId, Balance, PanicOnDefault};

pub mod account;
pub use crate::account::*;

pub mod board;
pub use crate::board::*;

#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc<'_> = near_sdk::wee_alloc::WeeAlloc::INIT;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Place {
    pub account_indices: UnorderedMap<AccountId, u32>,
    pub board: board::PixelBoard,
    pub accounts: Vector<Account>,
}

#[near_bindgen]
impl Place {
    #[init]
    pub fn new() -> Self {
      assert!(!env::state_exists(), "Already initialized");
      let mut place = Self {
        account_indices: UnorderedMap::new(b"i".to_vec()),
        board: PixelBoard::new(),
        accounts: Vector::new(b"a".to_vec()),
      };

      let mut account = place.get_account_by_id(env::current_account_id());
      account.num_pixels = TOTAL_NUM_PIXELS;
      place.save_account(&account);

      place
    }

    #[payable]
    pub fn buy_tokens(&mut self) {
      unimplemented!();
    }

    pub fn draw(&mut self, pixels: Vec<SetPixelRequest>) {
      let mut account = self.get_account_by_id(env::predecessor_account_id());
      let new_pixels = pixels.len() as u32;
      account.charge(new_pixels);

      let mut old_owners = self.board.set_pixels(account.account_index, &pixels);
      let replaced_pixels = old_owners.remove(&account.account_index).unwrap_or(0);
      account.num_pixels += new_pixels - replaced_pixels;
      self.save_account(&account);

      for (account_index, num_pixels) in old_owners {
        let mut account = self.get_account_by_index(account_index).unwrap();
        account.num_pixels -= num_pixels;
        self.save_account(&account);
      }
    }
}


#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
    use super::*;

    use near_sdk::{testing_env, MockedBlockchain, VMContext};

    pub fn get_context(block_timestamp: u64, is_view: bool) -> VMContext {
      VMContext {
        current_account_id: "place.meta".to_string(),
        signer_account_id: "place.meta".to_string(),
        signer_account_pk: vec![0, 1, 2],
        predecessor_account_id: "place.meta".to_string(),
        input: vec![],
        block_index: 1,
        block_timestamp,
        epoch_height: 1,
        account_balance: 10u128.pow(26),
        account_locked_balance: 0,
        storage_usage: 10u64.pow(6),
        attached_deposit: 0,
        prepaid_gas: 300 * 10u64.pow(12),
        random_seed: vec![0, 1, 2],
        is_view,
        output_data_receivers: vec![],
      }
    }

    #[test]
    fn test_new() {
      let mut context = get_context(3_600_000_000_000, false);
      testing_env!(context.clone());
      let contract = Place::new();

      context.is_view = true;
      testing_env!(context.clone());
      assert_eq!(
        contract.get_pixel_cost().0,
        PIXEL_COST
      );
      assert_eq!(
        contract.get_line_versions(),
        vec![0u32; BOARD_HEIGHT as usize]
      )
    }
}

As usual, we have the new function to perform initialization. We also start by assigning all pixels to a single account (perhaps the account which the contract is deployed to).

And the ability to draw is another important aspect here. Though, note that buy_tokens is unimplemented; you can check Berry club contract for more information if you want. That's the ability to buy fungible tokens, which first requires you to write a contract for fungible tokens, then port it in here. In Berryclub, these are cucumber, avocado, and bananas (may update in the future).

The draw logic includes replacing the old owner for each Pixel to its new owner.

Note: if at this point things won't run well, especially with u16 error, please replace all of them with u32.

Testing

Of course we need testing. Here we'll look at a simple unit test, to test the creation of new board is successful.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::{UnorderedMap, Vector};
use near_sdk::{env, near_bindgen, AccountId, Balance, PanicOnDefault};

pub mod account;
pub use crate::account::*;

pub mod board;
pub use crate::board::*;

#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc<'_> = near_sdk::wee_alloc::WeeAlloc::INIT;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Place {
    pub account_indices: UnorderedMap<AccountId, u32>,
    pub board: board::PixelBoard,
    pub accounts: Vector<Account>,
}

#[near_bindgen]
impl Place {
    #[init]
    pub fn new() -> Self {
      assert!(!env::state_exists(), "Already initialized");
      let mut place = Self {
        account_indices: UnorderedMap::new(b"i".to_vec()),
        board: PixelBoard::new(),
        accounts: Vector::new(b"a".to_vec()),
      };

      let mut account = place.get_account_by_id(env::current_account_id());
      account.num_pixels = TOTAL_NUM_PIXELS;
      place.save_account(&account);

      place
    }

    #[payable]
    pub fn buy_tokens(&mut self) {
      unimplemented!();
    }

    pub fn draw(&mut self, pixels: Vec<SetPixelRequest>) {
      let mut account = self.get_account_by_id(env::predecessor_account_id());
      let new_pixels = pixels.len() as u32;
      account.charge(new_pixels);

      let mut old_owners = self.board.set_pixels(account.account_index, &pixels);
      let replaced_pixels = old_owners.remove(&account.account_index).unwrap_or(0);
      account.num_pixels += new_pixels - replaced_pixels;
      self.save_account(&account);

      for (account_index, num_pixels) in old_owners {
        let mut account = self.get_account_by_index(account_index).unwrap();
        account.num_pixels -= num_pixels;
        self.save_account(&account);
      }
    }
}


#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
    use super::*;

    use near_sdk::{testing_env, MockedBlockchain, VMContext};

    pub fn get_context(block_timestamp: u64, is_view: bool) -> VMContext {
      VMContext {
        current_account_id: "place.meta".to_string(),
        signer_account_id: "place.meta".to_string(),
        signer_account_pk: vec![0, 1, 2],
        predecessor_account_id: "place.meta".to_string(),
        input: vec![],
        block_index: 1,
        block_timestamp,
        epoch_height: 1,
        account_balance: 10u128.pow(26),
        account_locked_balance: 0,
        storage_usage: 10u64.pow(6),
        attached_deposit: 0,
        prepaid_gas: 300 * 10u64.pow(12),
        random_seed: vec![0, 1, 2],
        is_view,
        output_data_receivers: vec![],
      }
    }

    #[test]
    fn test_new() {
      let mut context = get_context(3_600_000_000_000, false);
      testing_env!(context.clone());
      let contract = Place::new();

      context.is_view = true;
      testing_env!(context.clone());
      assert_eq!(
        contract.get_pixel_cost().0,
        PIXEL_COST
      );
      assert_eq!(
        contract.get_line_versions(),
        vec![0u32; BOARD_HEIGHT as usize]
      )
    }
}

End Note

Of course, the listings aren't totally correct. The only tested ones are the final ones, which is listing-13. If you want to read the working ones (no errors), read listing-13. Otherwise, go to the original Github repo for the code.

Next, we'll take a look at frontend code.

Resources

Frontend

The first thing to note is, this is created using Create-React-App, as mentioned in the repo. So just go to that link and follow how to create a React App, before starting.

After you bootstrapped the app, go to src, and you'll find several items in it. Compare that to the repo, and if you find differences, just delete them and rewrite them. That will do.

Next, change the package.json to fit the ones you found in repo. You could also make changes to the versions yourself, just make sure that it'll still run fine if that's the case.

Another thing is one removed the building of gh-pages as we don't need that here.

Of course the frontend is important. In fact, whether end users want to use your product depends on how well you designed your frontend, not the functionalities of your backend, for first impression. Only then the backend functionalities come as second impression. You could be a really good contract developer and no one will use your product if your frontend is not attractive compared to alternative solutions by your competitors that might not have the best backend (at least might not be as good as yours) but have a flattering, easy to use, easy to understand, frontend.

One ever read in a book written by Basecamp (Jason and/or DHH) mentioning that: (rephrased)

The first thing to design is your frontend. Whether or not to include a functionality depends on whether you have enough space on your frontend, whether it'll make it cluttered, whether it's necessary, etc.

And honestly speaking, I'm a backend developer, not a frontend. At least, one never did UI design before, nor writing frontend code.

And for frontend, the provided frontend in NEAR contracts might not be the best; most people are backend developers with some knowledge of frontend to make it usable, not to impress. You might prefer to learn from professional UI designer for better UI design, better UX, etc.

Another thing to think about, aside from looking good, is "words expression". Meaning, how well do you express what you want to convey, and how simple it is such that it could be understood by most audiences, not just native English speakers? Do you have support for other languages, so native speakers in other languages doesn't feel foreign? This also means that learning how to write well is another thing. All in all, frontend is like trying to communicate to other people: if you cannot phrase your words well, others cannot understand. Usually when we speak we might complain that others cannot understand how we speak; but actually that is the wrong consideration, as we're the one trying to convey something to others, so the most important thing is whether other understand what you're trying to convey or not, and the conveyor requires to fit to the listener for his/her best experience rather than the listener fitting to the conveyor trying to understand what is being conveyed. Do understand that the listener could choose not to listen to you if you cannot convey it clearly, in his language, making it easy for him/her to understand; but you can't afford to lose your listeners as it means losing a source of income if you're trying to ask people to use your product.

A lot of the things we have are frontend configuration, and one won't talk about it in depth, as different design have different code. You can look more into it, as there are more tutorials from BerryClub about this. There are indeed one more important ones that one want to talk about, is how to connect with the backend. After you deploy your contract, how does your frontend interact with the deployed backend, which is hosted on-chain?

  async _init_Near() {
    const near_config = {
      network_id: 'default',
      node_url: 'https://rpc.testnet.near.org',
      contract_name: ContractName,
      wallet_url: 'https://wallet.testnet.near.org',
    };
    const keystore = new nearAPI.keyStores.BrowserLocalStorageKeyStore();
    const near = await nearAPI.connect(Object.assign({
      deps: { keystore }
    }, near_config));

    this._keystore = keystore;
    this._near_config = near_config;
    this._near = near;

    this._wallet_connection = new nearAPI.WalletConnection(near, ContractName);
    this._account_id = this._wallet_connection.getAccountId();

    this._account = this._wallet_connection.account();
    this._contract = new nearAPI.Contract(this._account, ContractName, {
      viewMethods: ['get_lines', 'get_line_versions', 
      'get_pixel_cost', 'get_account_balance', 
      'get_account_num_pixels', 'get_account_id_by_index'
      ],
      changeMethods: ['draw', 'buy_tokens'],
    });

    this._pixel_cost = parseFloat(
      await this._contract.get_pixel_cost()
    );

    if (this._account_id) {
      await this.refresh_account_stats();
    }

    this._line_versions = Array(BoardHeight).fill(-1);
    this._lines = Array(BoardHeight).fill(false);
    await this.refresh_board(true);
  }

The important thing here is App.js being the main place where design are done.

(P.S. Given how I love using snake-case for functions and variables and JS opposes that rule, and you can see a mixture of snake-case for custom-defined functions and variables above while camelCase for predefined ones. Don't follow one's way of doing it, though. )

This speaks about the core of the program: the others are mostly just the design and speaks of interaction with users (how to react to user inputs); this function speaks with the backend: how frontend interacts with backend. We use the near-api-js library to deal with the interaction.

The above function includes configuration to connect to testnet (check for more information), how to connect to the keystore and the wallet, and define the methods available in the smart contract manually, after you decide what goes into viewMethods and what goes to changeMethods. Other things that goes in here are more specific towards the application, like initialization requires to know the specification of Board at the start rather than Null, and how much each PixelCost.

  async request_sign_in() {
    const app_title = 'NEAR Place';
    await this._wallet_connection.requestSignIn(
      ContractName,
      app_title
    )
  }

  async logout() {
    this._wallet_connection.signOut();
    this._account_id = null;
    this.setState({
      signed_in: !!this._account_id,
      account_id: this._account_id,
    })
  }

There exist two additional public functions which allow users to sign in and logout of their accounts.

That's it we'll talk about today. It's a good idea to look at more contracts, so let's do that. Next, we'll look at another contract

References

Gonear-name (THINKING OF REMOVING)

The next thing we would look at is this gonear-name contract by Kikimora-Labs. One isn't sure how one found this contract, but there are some interesting stuffs going on here that one thought we might want to take a look at.

There will be some difference here. First, we use the newest near-sdk available at this time, "=4.0.0-pre.4", rather than the original "=3.0.0-pre.2", so we can use some of the latest updates.

Second, one won't go and discuss everything in this chapter anymore. Most requires you to read the contract yourself (click on the link in References for the original Github repo); one will discuss those that are more helpful from one's perspective.

References

Lock unlock account

The first is how to lock and unlock an account. These are the definitions as usual.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

Note the decorator ext_contract(ext_self) means this is targeted for cross-contract calls. For more information, please refer to this page. Cross-contract calls are also known as "Callbacks".

Note since we're using near-sdk-rs v4 instead of v3, we shall also use some newest features like require! to replace assert!, assert_eq! and assert_ne!.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

The above function checks if a promise is successful. One actually doesn't know what a promise is before this (not a web developer anyways), but one learnt from the near-sdk rust doc it's something that won't be called immediately, but called on the future. What it means by future, according to the doc, is it's at least the next "block height" which it will execute, rather than at current "block height". If you don't know about how time is calculated, check out "Bitcoin is Time" article on the notable page.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

With init(ignore_state), this is an initialization method, where immediately when this started, this method will be called to first lock the account, regardless of what state it's currently in, for safety reason. Otherwise if it's in unlock state, anybody could access the account at initialization.

Only certain people can lock an account. In this case, predecessor means the previous person whom have ownership over the content, which should be the owner whom unlock it in the first place. Hence, if the current is the owner, (hence current == predecessor), we allow him/her to lock back his/her account. We don't allow a random person to come in and lock other people's account.

If you check the docs for AccountId, we found that the From implementation (under "Trait Implementation") is From<ValidAccountId>, so that means, calling AccountId.into() will convert the result to ValidAccountId type. This is just basic Rust, if you already know, great! If you don't know yet, hope you understand now.

In the same way, we see owner_id here is of type ValidAccountId but the definition requires owner_id to be AccountId, the .into() will convert it back to AccountId type.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

When we lock the account, we have a certain owner_id that we stored away. The owner_id is hypothesized to most probably be the current_account_id, otherwise it wouldn't pass the assertion. Here, we're retrieving the owner_id to compare that it matches, predecessor has not changed in some way, so initialize the unlocking.

Note: One isn't sure why it isn't compared to env::current_account_id() here though. Perhaps the contract can be hold by several person, each with different owner_id, so this might be checking that the correct contract is calling the correct predecessor? One don't know.

Originally they use AccountId::default(). Searching the docs for that particular version of near-sdk, one cannot find the impl Default trait for that. So, one read through the logic and assume that they're trying to set it back to None. However, one doesn't know how to do that. In fact, one think that since you are assigning it again when locking, perhaps we could let it be while unlocked. But what if someone hack and steal your AccountId while unlocked? One don't know.

Then we have a Promise to give a FullAccessKey to the owner of the account by the end.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

get_owner is just a view method, easy to understand. on_access_key_added is a callback, which is called from the Promise previously ext_self::on_access_key_added. Note that callbacks are usually called by the contract internally, hence usually it is fulfilled via a Promise. You don't want other people to call callbacks externally, as that can lead your contract to be vulnerable. We have to check that it's certainly called by "the" contract. This means the predecessor equals the current. The current is the one holding the contract, and he's also the previous owner of the contract (the contract never gets redeployed anywhere, eh, so you get the logic).

References

Other contracts

There are other contracts like the escrow-contract which we won't look at it now. Indeed, we'll have an independent section for "locking/vesting contract" here instead.

We would also skip the marketplace contract

Vesting/Lockup Contract

This is a pure contract (no frontend). Basically, this contract acts as "escrow" to lock up others' token for a period of time, for whatever reason.

Lockup and vesting have some differences. In particular, Vesting has 2 extra configurations:

  1. You can terminate the vesting and refund the non-vested tokens.
  2. Cliff vesting

In NEAR, the most common uses of lockup contract is during Staking. Owner whom stake their tokens have their tokens locked-up, and unstaking requires an amount of time before the lockup is unlocked.

For more info, check out their readme (link in references section). Without further ado, let's start looking at the contracts.

Extra information on the contract

#TODO: The contract consists of two parts that are intertwined with each other: the owner and the foundation. foundation are linked to NEAR Foundation, while owner are linked to (to be updated).

Some further details on the programs

For this, we shall use near-sdk-4.0.0-pre.7, as opposed to the stable version 3.1.0 used in the contract. This means we might get lots of error (if you use VSCode rust-analyzer extension) about near_sdk not found. That's because they check for only stable ones, not pre-release versions.

We would also talk about simulation tests in this chapter, although not in depth.

As usual, we would skip those explanations for things that are self-explanatory. Those that requires further explanation will have further explanations.

NOTE: If you call cargo test, it's gonna take quite long to build, and it's gonna take up GBs of your space. So if you're renting the VM, make sure you have at least 12GB to build the thingy, otherwise your VM might get stuck, unable to shut down (well, force shut down takes a long while, at least for Azure VM) and perhaps other problems be introduced.

References

Lockup Contract

Ultimately, the core of the contract is the Lockup Contract. However, there are several implementations of the Lockup Contract based on which do we need. These implementations are splitted among different Rust files, and we'll look at them files by files in this case.

However, to get you started with what the Lockup Contract is, we shall first take a look at the default implementation and the struct located in lib.rs.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

This should give you some information on the LockupContract that we'll implement in the next few pages.

References

Types

We can't really continue without first defining all the types that will be used in this contract. Let's take a look at these types.

These may be lots of information, but don't worry. You don't necessary have to know all of them at once. You can alway have this page on half of your screen (or another screen), and continue reading the pages on another screen, so you can always refer back to what things are while reading through. This page aims more as a reference than explanations.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

We don't have 256-bit unsigned integer in the near-sdk, so we need to construct it ourselves.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

Some basic types that we have declared.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

This contains the lockup information. The explanation are in the documentation inside the code.

The rest will be here for you to see what information is included, we won't necessary explain what they are as there are documentations to explain in code, unless necessary to clear doubts.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

For vesting, there include some implementations to assert its validity. What it's checking for is very clear in the code.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

One actually doesn't know why Deserialize isn't derived from the above enum.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

Note that the below is deprecated. Actually, they're deprecating the private vesting, so this includes the VestingScheduleWithSalt used only in private vesting.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base64VecU8, U128, U64};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, AccountId, Balance, require};
use uint::construct_uint;

construct_uint! {
    pub struct U256(4);
}

pub type Duration = u64;  // in nanoseconds
pub type Timestamp = u64;  // nanoseconds. 

// Wrapped into a struct for JSON serialization as string. 
pub type WrappedTimestamp = U64;
pub type WrappedDuration = U64;
pub type WrappedBalance = U128;

/// Hash for Vesting Schedule. 
pub type Hash = Vec<u8>;

/// The result of the transfer poll. 
/// Contains the timestamp when the proposal was voted in. 
pub type PollResult = Option<WrappedTimestamp>;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupInformation {
    pub lockup_amount: Balance,  // yoctoNEAR

    /// Early termination withdrawal. 
    /// Accounted separately from lockup_amount to ensure
    /// linear release not affected. 
    pub termination_withdrawn_tokens: Balance,

    /// [deprecated] lockup duration in nanoseconds. 
    /// Release doesn't start. 
    /// Replaced with `lockup_timestamp` and `release_duration`
    pub lockup_duration: Duration,

    /// If present, the duration when the full lockup amount
    /// be available. The tokens are linearly released from
    /// the moment tokens are unlocked, defined by:
    /// `max(transfer_timestamp + lockup_duration, lockup_timestamp)`.
    /// If absent, tokens aren't locked (vesting logic could
    /// be used, though).
    pub release_duration: Option<Duration>,

    /// Optional absolute lockup timestamp in nanoseconds. 
    /// Lock tokens until timestamp passes; then release starts after.
    /// If absent, `transfers_timestamp` will be used. 
    pub lockup_timestamp: Option<Timestamp>,

    /// Information about transfers. 
    /// If present: contains timestamp of when it was enabled.
    /// If absent: contains AccountId or transfer poll contract.
    pub transfers_information: TransfersInformation,
}

/// Contains information about the transfer, whether they're 
/// enabled or disabled. 
#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum TransfersInformation {

    /// Timestamp when transfers were enabled.
    TransfersEnabled {
        transfers_timestamp: WrappedTimestamp,
    },

    /// AccountId of transfers poll contract, to check if transfers
    /// are enabled. Lockup period starts only after transfer voted
    /// to be enabled. Starts with disabled transfer. Once transfers 
    /// are enabled, they can't be disabled and don't require 
    /// further checking. 
    TransfersDisabled {
        transfer_poll_account_id: AccountId
    },
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, 
  Serialize, PartialEq
)]
#[serde(crate = "near_sdk::serde")]
pub enum TransactionStatus {
    Idle,  // no transaction in progress
    Busy,  // transaction in progress.
}

/// Contains information about current stake and delegation.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct StakingInformation {
    pub staking_pool_account_id: AccountId,

    /// Whether there is a transaction in progress.
    pub status: TransactionStatus,

    /// Amount of token deposited from this account to staking 
    /// pool. NOTE: unstaked amount on staking pool might be 
    /// higher due to staking rewards.
    pub deposit_amount: WrappedBalance,
}

#[derive(BorshDeserialize, BorshSerialize, Deserialize, Serialize, Clone, PartialEq, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingSchedule {
    /// vesting starts in nanoseconds
    pub start_timestamp: WrappedTimestamp, 

    /// In nanoseconds, when the FIRST PART of lockup tokens
    /// become vested. The remaining tokens will vest 
    /// continuously until they're fully vested. 
    pub cliff_timestamp: WrappedTimestamp,

    /// vesting ends in nanoseconds
    pub end_timestamp: WrappedTimestamp,
}


impl VestingSchedule {
    pub fn assert_valid(&self) {
      require!(
        self.start_timestamp.0 <= self.cliff_timestamp.0,
        "Cliff timestamp cannot be earlier than start timestamp."
      );

      require!(
        self.cliff_timestamp.0 <= self.end_timestamp.0,
        "Cliff timestamp cannot be later than end timestamp."
      );

      require!(
        self.start_timestamp.0 < self.end_timestamp.0,
        "Total vesting time should be positive."
      );
    }
}

/// Initialization argument type to define vesting schedule
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingScheduleOrHash {
    
    /// [deprecated] Only public schedule is used after transfers
    /// enabled. Vesting schedule is private, and this is the
    /// hash of (vesting_schedule, salt). In JSON, hash has to 
    /// be encoded with base64 to string. 
    VestingHash(Base64VecU8),

    /// Vesting schedule (public)
    VestingSchedule(VestingSchedule),
}

/// Contains information about vesting that contains vesting schedule
/// and termination information.
#[derive(
  Serialize, BorshDeserialize, BorshSerialize, 
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum VestingInformation {
    None,

    /// [deprecated] for private vesting. Hashed for privacy and only
    /// revealed if NEAR foundation terminate vesting. Assumes it
    /// doesn't affect lockup release and duration.
    VestingHash(Base64VecU8),

    /// Explicit vesting schedule
    VestingSchedule(VestingSchedule),

    /// Info about early termination, currently in progress. 
    /// Once unvested amount is transferred out, `VestingInformation`
    /// is removed.
    Terminating(TerminationInformation),
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Copy, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub enum TerminationStatus {
    
    /// Initial stage of termination in case deficit present
    /// on account. 
    VestingTerminatedWithDeficit,

    /// Transaction to unstake everything in progress.
    UnstakingInProgress,

    /// Unstaking from staking pool completed.
    EverythingUnstaked,

    /// Withdraw everything from staking pool, in progress.
    WithdrawingFromStakingPoolInProgress,

    /// Everything out of staking pool. Ready to withdraw 
    /// to wallet. (out of account)
    ReadyToWithdraw,

    /// Withdraw tokens from account (to wallet) in progress.
    WithdrawingFromAccountInProgress,
}

#[derive(
  BorshDeserialize, BorshSerialize, Deserialize, Serialize,
  PartialEq, Clone, Debug
)]
#[serde(crate = "near_sdk::serde")]
pub struct TerminationInformation {

    /// The amount of unvested token has to be transferred back
    /// to NEAR Foundation. These tokens are effectively locked
    /// and can't be transferred out and can't be restaked.
    pub unvested_amount: WrappedBalance,

    /// Status of withdrawal. When unvested amount is in progress
    /// of withdrawal, the status marked as busy, to avoid 
    /// withdrawing the funds twice. 
    pub status: TerminationStatus,
}

#[derive(BorshSerialize, Deserialize, Serialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct VestingScheduleWithSalt {
    pub vesting_schedule: VestingSchedule,

    /// salt to make hash unique.
    pub salt: Base64VecU8,
}


impl VestingScheduleWithSalt {
    pub fn hash(&self) -> Hash {
      env::sha256(
        &self.try_to_vec().expect("Failed to serialize")
      )
    }
}

Let's move on.

Foundation

The first page one sees is the foundation.rs at the very top of the list of Rust files on GitHub. This contract is targeted towards NEAR Foundation accounts, "foundation" here doesn't mean it's the foundation of the smart contract (meaning it's most important functions), but NEAR Foundation's "foundation". So let's take a look at it.

use near_sdk::{near_bindgen, Promise, require, AccountId};
// use near_account_id::AccountId;
use std::cmp;

use crate::*;

#[near_bindgen]
impl LockupContract{
    /// FOUNDATION'S METHOD
    /// 
    /// Requires 25 TGas (1 * BASE_GAS)
    /// 
    /// Terminates vesting schedule and locks the remaining
    /// unvested amount. If the lockup contract was initialized
    /// with the private vesting schedule, then this method 
    /// expects to receive a `VestingScheduleWithSalt` to
    /// reveal the vesting schedule; otherwise expects `None`. 
    pub fn terminate_vesting(
      &mut self,
      vesting_schedule_with_salt: Option<VestingScheduleWithSalt>,
    ) {
      self.assert_called_by_foundation();

      let vesting_schedule = self.assert_vesting(vesting_schedule_with_salt);
      let unvested_amount = self.get_unvested_amount(vesting_schedule);
      require!(
        unvested_amount.0 > 0,
        "The account is fully vested"
      );

      env::log_str(
        format!(
          "Terminating vesting. The remaining unvested balance is {}",
          unvested_amount.0
        ).as_str()
      );

      let deficit = unvested_amount
          .0
          .saturating_sub(self.get_account_balance().0);

      // If there's deficit in liquid balance and a staking pool is selected,
      // then the contract will try to withdraw everything from this staking
      // pool to cover deficit. 
      let status = if deficit > 0 && self.staking_information.is_some() {
        TerminationStatus::VestingTerminatedWithDeficit
      } else {
        TerminationStatus::ReadyToWithdraw
      };

      self.vesting_information = VestingInformation::Terminating(
        TerminationInformation {
          unvested_amount, 
          status,
        }
      );
    }

    /// FOUNDATION'S METHOD
    /// 
    /// Requires 175 TGas (7 * BASE_GAS)
    /// 
    /// When vesting is terminated and there are deficit of the 
    /// tokens on the account, the deficit amount of tokens has to
    /// be unstaked and withdrawn from the staking pool. 
    /// This includes unstaking everything + waiting for 4 epochs
    /// to prepare for withdrawal. 
    pub fn termination_prepare_to_withdraw(&mut self) -> Promise {
      self.assert_called_by_foundation();
      self.assert_staking_pool_is_idle();

      let status = self.get_termination_status();

      match status {
        None => {
          env::panic_str("There is no termination in progress.");
        }

        Some(TerminationStatus::UnstakingInProgress)
        | Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        | Some(TerminationStatus::WithdrawingFromAccountInProgress) => {
          env::panic_str("Another transaction already in progress.");
        }

        Some(TerminationStatus::ReadyToWithdraw) => {
          env::panic_str("The account is ready to withdraw unvested balance.")
        }

        Some(TerminationStatus::VestingTerminatedWithDeficit) => {
          // Need to unstake
          self.set_termination_status(TerminationStatus::UnstakingInProgress);
          self.set_staking_pool_status(TransactionStatus::Busy);
          env::log_str("Termination Step: Unstaking everything from staking pool.");

          ext_staking_pool::get_account_staked_balance(
            env::current_account_id(),
            self
                .staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone(),
            NO_DEPOSIT,
            gas::staking_pool::GET_ACCOUNT_STAKED_BALANCE,
          )
          .then(
            ext_self_foundation::on_get_account_staked_balance_to_unstake(
              env::current_account_id(),
              NO_DEPOSIT,
              gas::foundation_callbacks::ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE,
            ),
          )
        }

        Some(TerminationStatus::EverythingUnstaked) => {
          // Need to withdraw everything

          self.set_termination_status(
            TerminationStatus::WithdrawingFromStakingPoolInProgress,
          );
          self.set_staking_pool_status(TransactionStatus::Busy);
          
          env::log_str("Termination Step: Withdraw everything from staking pool.");

          ext_staking_pool::get_account_unstaked_balance(
            env::current_account_id(),
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone(),
            NO_DEPOSIT,
            gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
          )
          .then(
            ext_self_foundation::on_get_account_unstaked_balance_to_withdraw(
              env::current_account_id(),
              NO_DEPOSIT,
              gas::foundation_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW,
            ),
          )
        }
      }
    }

    /// FOUNDATION'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Withdraws the unvested amount from the early termination of the 
    /// vesting schedule.
    pub fn termination_withdraw(&mut self, receiver_id: AccountId) -> Promise {
      self.assert_called_by_foundation();

      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid."
      );

      require!(
        self.get_termination_status() == Some(TerminationStatus::ReadyToWithdraw),
        "Termination status (Funds) is not ready to withdraw"
      );

      let amount = cmp::min(
        self.get_terminated_unvested_balance().0,
        self.get_account_balance().0
      );

      require!(
        amount > 0,
        "Insufficient liquid balance (amount) to withdraw."
      );

      env::log_str(
        format!(
          "Termination Step: Withdrawing {} of terminated unvested balance to @{}",
          amount, receiver_id
        )
        .as_str(),
      );

      self.set_termination_status(TerminationStatus::WithdrawingFromAccountInProgress);

      Promise::new(receiver_id.clone()).transfer(amount).then(
        ext_self_foundation::on_withdraw_unvested_amount(
          amount.into(),
          receiver_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::foundation_callbacks::ON_WITHDRAW_UNVESTED_AMOUNT,
        ),
      )
    }
}



The first assertion checks whether the account is fully vested or not. Based on this definition of "fully vested", we can understand that the "vested amount" are the ones ready to withdraw. So the first assertion unvested_amount.0 > 0 tells you that, "hey, there are still some tokens in lockup (vesting), so you can terminate the lockup (vesting) process to withdraw them. In contrast, the error message "The account is fully vested" actually means "Hey, you have nothing locked up, so you have nothing to terminate".

Note that saturating_sub isn't part of near-sdk-rs but standard function inside Rust.

There are some functions that makes sense, but we'll see them later like self.assert_called_by_foundation(). Now, one wants to talk about one of the internal functions, called self.get_account_balance().

use near_sdk::require;
use crate::*;

/*********************
 * Internal Methods
 *********************/

impl LockupContract {
    /// Balance excluding storage staking balance. 
    /// Storage staking balance can't be transferred out
    /// without deleting this contract.
    pub(crate) fn get_account_balance(&self) -> WrappedBalance {
      env::account_balance()
          .saturating_sub(MIN_BALANCE_FOR_STORAGE)
          .into()
    }

    pub(crate) fn set_staking_pool_status(
      &mut self,
      status: TransactionStatus
    ) {
      self.staking_information
          .as_mut()
          .expect("Staking pool should be selected.")
          .status = status;
    }

    pub(crate) fn set_termination_status(
      &mut self,
      status: TerminationStatus
    ) {
      if let VestingInformation::Terminating(termination_information) = 
          &mut self.vesting_information 
      {
        termination_information.status = status;
      } else {
        unreachable!("Vesting information not at terminating stage. ");
      }
    }

    pub(crate) fn assert_vesting(
      &self,
      vesting_schedule_with_salt: Option<VestingScheduleWithSalt>,
    ) -> VestingSchedule {
      match &self.vesting_information {

        VestingInformation::VestingHash(hash) => {
          if let Some(vesting_schedule_with_salt) = vesting_schedule_with_salt {
            require!(
              &vesting_schedule_with_salt.hash() == &hash.0,
              "Presented vesting schedule and salt don't match the hash."
            );
            vesting_schedule_with_salt.vesting_schedule
          } else {
            env::panic_str("Expected vesting schedule and salt, but not provided.")
          }
        }

        VestingInformation::VestingSchedule(vesting_schedule) => {
          require!(
            vesting_schedule_with_salt.is_none(),
            "Explicit vesting schedule already exists."
          );
          vesting_schedule.clone()
        }

        VestingInformation::Terminating(_) => env::panic_str("Vesting was terminated."),

        VestingInformation::None => env::panic_str("Vesting is None."),
      }
    }

    pub(crate) fn assert_staking_pool_is_idle(&self) {
      require!(
        self.staking_information.is_some(),
        "Staking pool is not selected."
      );

      match self.staking_information.as_ref().unwrap().status {
        
        TransactionStatus::Idle => (),

        TransactionStatus::Busy => {
          env::panic_str("Contract currently busy with another operation. ")
        }
      };
    }

    pub(crate) fn assert_staking_pool_is_not_selected(&self) {
      require!(
        self.staking_information.is_none(),
        "Staking pool is already selected"
      );
    }

    pub(crate) fn assert_no_termination(&self) {
      if let VestingInformation::Terminating(_) = &self.vesting_information {
        env::panic_str(
          "All operations are blocked until vesting termination is completed." 
        );
      }
    }



    pub(crate) fn assert_called_by_foundation(&self) {
      if let Some(foundation_account_id) = &self.foundation_account_id {
        require!(
          &env::predecessor_account_id() == foundation_account_id,
          "Can only be called by NEAR Foundation"
        )
      } else {
        env::panic_str("No NEAR Foundation account specified in the contract.");
      }
    }


    pub(crate) fn assert_owner(&self) {
      require!(
        &env::predecessor_account_id() == &self.owner_account_id,
        "This method can only be called by the owner. "
      )
    }

    pub(crate) fn assert_transfers_enabled(&self) {
      require!(
        self.are_transfers_enabled(),
        "Transfers are disabled."
      );
    }

    pub(crate) fn assert_transfers_disabled(&self) {
      require!(
        !self.are_transfers_enabled(),
        "Transfers are already enabled."
      );
    }


    pub(crate) fn assert_no_staking_or_idle(&self) {
      if let Some(staking_information) = &self.staking_information {
        match staking_information.status {
          TransactionStatus::Idle => (),
          TransactionStatus::Busy => {
            env::panic_str("Contract is currently busy with another operation.")
          }
        };
      }
    }


    pub(crate) fn staking_pool_account_id_clone(&self) -> AccountId {
      self.staking_information
          .as_ref()
          .unwrap()
          .staking_pool_account_id
          .clone()
    }
}

As stated in the explanation, this function will return the account balance excluding those that are reserved. The reserved funds are for storage and transactions, and they could be seen in your NEAR Wallet as well if you navigate to the "Account" page.

Account Page of NEAR Wallet. See there are "Reserved for Storage" and "Reserved for transactions".

Another popular one is the assert_called_by_foundation. This checks whether the predecessor (which account is evoking the method directly) is actually the foundation's account.

These are the most important, the rest of the hidden ones, we'll take a look as we get there, but they should be easy to understand based on their names.

Next, let's take a look at termination_prepare_to_withdraw.

use near_sdk::{near_bindgen, Promise, require, AccountId};
// use near_account_id::AccountId;
use std::cmp;

use crate::*;

#[near_bindgen]
impl LockupContract{
    /// FOUNDATION'S METHOD
    /// 
    /// Requires 25 TGas (1 * BASE_GAS)
    /// 
    /// Terminates vesting schedule and locks the remaining
    /// unvested amount. If the lockup contract was initialized
    /// with the private vesting schedule, then this method 
    /// expects to receive a `VestingScheduleWithSalt` to
    /// reveal the vesting schedule; otherwise expects `None`. 
    pub fn terminate_vesting(
      &mut self,
      vesting_schedule_with_salt: Option<VestingScheduleWithSalt>,
    ) {
      self.assert_called_by_foundation();

      let vesting_schedule = self.assert_vesting(vesting_schedule_with_salt);
      let unvested_amount = self.get_unvested_amount(vesting_schedule);
      require!(
        unvested_amount.0 > 0,
        "The account is fully vested"
      );

      env::log_str(
        format!(
          "Terminating vesting. The remaining unvested balance is {}",
          unvested_amount.0
        ).as_str()
      );

      let deficit = unvested_amount
          .0
          .saturating_sub(self.get_account_balance().0);

      // If there's deficit in liquid balance and a staking pool is selected,
      // then the contract will try to withdraw everything from this staking
      // pool to cover deficit. 
      let status = if deficit > 0 && self.staking_information.is_some() {
        TerminationStatus::VestingTerminatedWithDeficit
      } else {
        TerminationStatus::ReadyToWithdraw
      };

      self.vesting_information = VestingInformation::Terminating(
        TerminationInformation {
          unvested_amount, 
          status,
        }
      );
    }

    /// FOUNDATION'S METHOD
    /// 
    /// Requires 175 TGas (7 * BASE_GAS)
    /// 
    /// When vesting is terminated and there are deficit of the 
    /// tokens on the account, the deficit amount of tokens has to
    /// be unstaked and withdrawn from the staking pool. 
    /// This includes unstaking everything + waiting for 4 epochs
    /// to prepare for withdrawal. 
    pub fn termination_prepare_to_withdraw(&mut self) -> Promise {
      self.assert_called_by_foundation();
      self.assert_staking_pool_is_idle();

      let status = self.get_termination_status();

      match status {
        None => {
          env::panic_str("There is no termination in progress.");
        }

        Some(TerminationStatus::UnstakingInProgress)
        | Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        | Some(TerminationStatus::WithdrawingFromAccountInProgress) => {
          env::panic_str("Another transaction already in progress.");
        }

        Some(TerminationStatus::ReadyToWithdraw) => {
          env::panic_str("The account is ready to withdraw unvested balance.")
        }

        Some(TerminationStatus::VestingTerminatedWithDeficit) => {
          // Need to unstake
          self.set_termination_status(TerminationStatus::UnstakingInProgress);
          self.set_staking_pool_status(TransactionStatus::Busy);
          env::log_str("Termination Step: Unstaking everything from staking pool.");

          ext_staking_pool::get_account_staked_balance(
            env::current_account_id(),
            self
                .staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone(),
            NO_DEPOSIT,
            gas::staking_pool::GET_ACCOUNT_STAKED_BALANCE,
          )
          .then(
            ext_self_foundation::on_get_account_staked_balance_to_unstake(
              env::current_account_id(),
              NO_DEPOSIT,
              gas::foundation_callbacks::ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE,
            ),
          )
        }

        Some(TerminationStatus::EverythingUnstaked) => {
          // Need to withdraw everything

          self.set_termination_status(
            TerminationStatus::WithdrawingFromStakingPoolInProgress,
          );
          self.set_staking_pool_status(TransactionStatus::Busy);
          
          env::log_str("Termination Step: Withdraw everything from staking pool.");

          ext_staking_pool::get_account_unstaked_balance(
            env::current_account_id(),
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone(),
            NO_DEPOSIT,
            gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
          )
          .then(
            ext_self_foundation::on_get_account_unstaked_balance_to_withdraw(
              env::current_account_id(),
              NO_DEPOSIT,
              gas::foundation_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW,
            ),
          )
        }
      }
    }

    /// FOUNDATION'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Withdraws the unvested amount from the early termination of the 
    /// vesting schedule.
    pub fn termination_withdraw(&mut self, receiver_id: AccountId) -> Promise {
      self.assert_called_by_foundation();

      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid."
      );

      require!(
        self.get_termination_status() == Some(TerminationStatus::ReadyToWithdraw),
        "Termination status (Funds) is not ready to withdraw"
      );

      let amount = cmp::min(
        self.get_terminated_unvested_balance().0,
        self.get_account_balance().0
      );

      require!(
        amount > 0,
        "Insufficient liquid balance (amount) to withdraw."
      );

      env::log_str(
        format!(
          "Termination Step: Withdrawing {} of terminated unvested balance to @{}",
          amount, receiver_id
        )
        .as_str(),
      );

      self.set_termination_status(TerminationStatus::WithdrawingFromAccountInProgress);

      Promise::new(receiver_id.clone()).transfer(amount).then(
        ext_self_foundation::on_withdraw_unvested_amount(
          amount.into(),
          receiver_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::foundation_callbacks::ON_WITHDRAW_UNVESTED_AMOUNT,
        ),
      )
    }
}



As usual, we'll leave some of the setters and getters method to later on, since the name explains what they're trying to do, hence we don't need to specially explain them here.

When we write the status, we want to fail hard, and fail early. Fail early, so we can save gas, and knows where our error are, so we always have the results leading to panicking at the top before solving for success results after all panic has confirmed false.

We shall take a look at the cross-contract calls, starting name with ext_, that are available in lib.rs. These are cross-contract calls, meaning sending the call requires some time, so it'll happen in the future; and future means we need to use Promise.

There are some functions called gas::, these are in the gas.rs, where it contains a list of constants for the gas fee to pay. We won't specifically look at it, we'll look at it when we need it. Here, we have two different gas required. Let's take a look at them.

The first is gas::staking_pool::GET_ACCOUNT_STAKED_BALANCE:

use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}

So we see, we just need the BASE amount of GAS for local processing in this case.

The second is gas::foundation_callbacks::ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE:

use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}
use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}

The Base, as previously mentioned, are for local processing (here called local updates, which means the same).

The unstaking part is also similar:

use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}
use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}
use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}

Finally is the termination_withdraw method.

use near_sdk::{near_bindgen, Promise, require, AccountId};
// use near_account_id::AccountId;
use std::cmp;

use crate::*;

#[near_bindgen]
impl LockupContract{
    /// FOUNDATION'S METHOD
    /// 
    /// Requires 25 TGas (1 * BASE_GAS)
    /// 
    /// Terminates vesting schedule and locks the remaining
    /// unvested amount. If the lockup contract was initialized
    /// with the private vesting schedule, then this method 
    /// expects to receive a `VestingScheduleWithSalt` to
    /// reveal the vesting schedule; otherwise expects `None`. 
    pub fn terminate_vesting(
      &mut self,
      vesting_schedule_with_salt: Option<VestingScheduleWithSalt>,
    ) {
      self.assert_called_by_foundation();

      let vesting_schedule = self.assert_vesting(vesting_schedule_with_salt);
      let unvested_amount = self.get_unvested_amount(vesting_schedule);
      require!(
        unvested_amount.0 > 0,
        "The account is fully vested"
      );

      env::log_str(
        format!(
          "Terminating vesting. The remaining unvested balance is {}",
          unvested_amount.0
        ).as_str()
      );

      let deficit = unvested_amount
          .0
          .saturating_sub(self.get_account_balance().0);

      // If there's deficit in liquid balance and a staking pool is selected,
      // then the contract will try to withdraw everything from this staking
      // pool to cover deficit. 
      let status = if deficit > 0 && self.staking_information.is_some() {
        TerminationStatus::VestingTerminatedWithDeficit
      } else {
        TerminationStatus::ReadyToWithdraw
      };

      self.vesting_information = VestingInformation::Terminating(
        TerminationInformation {
          unvested_amount, 
          status,
        }
      );
    }

    /// FOUNDATION'S METHOD
    /// 
    /// Requires 175 TGas (7 * BASE_GAS)
    /// 
    /// When vesting is terminated and there are deficit of the 
    /// tokens on the account, the deficit amount of tokens has to
    /// be unstaked and withdrawn from the staking pool. 
    /// This includes unstaking everything + waiting for 4 epochs
    /// to prepare for withdrawal. 
    pub fn termination_prepare_to_withdraw(&mut self) -> Promise {
      self.assert_called_by_foundation();
      self.assert_staking_pool_is_idle();

      let status = self.get_termination_status();

      match status {
        None => {
          env::panic_str("There is no termination in progress.");
        }

        Some(TerminationStatus::UnstakingInProgress)
        | Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        | Some(TerminationStatus::WithdrawingFromAccountInProgress) => {
          env::panic_str("Another transaction already in progress.");
        }

        Some(TerminationStatus::ReadyToWithdraw) => {
          env::panic_str("The account is ready to withdraw unvested balance.")
        }

        Some(TerminationStatus::VestingTerminatedWithDeficit) => {
          // Need to unstake
          self.set_termination_status(TerminationStatus::UnstakingInProgress);
          self.set_staking_pool_status(TransactionStatus::Busy);
          env::log_str("Termination Step: Unstaking everything from staking pool.");

          ext_staking_pool::get_account_staked_balance(
            env::current_account_id(),
            self
                .staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone(),
            NO_DEPOSIT,
            gas::staking_pool::GET_ACCOUNT_STAKED_BALANCE,
          )
          .then(
            ext_self_foundation::on_get_account_staked_balance_to_unstake(
              env::current_account_id(),
              NO_DEPOSIT,
              gas::foundation_callbacks::ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE,
            ),
          )
        }

        Some(TerminationStatus::EverythingUnstaked) => {
          // Need to withdraw everything

          self.set_termination_status(
            TerminationStatus::WithdrawingFromStakingPoolInProgress,
          );
          self.set_staking_pool_status(TransactionStatus::Busy);
          
          env::log_str("Termination Step: Withdraw everything from staking pool.");

          ext_staking_pool::get_account_unstaked_balance(
            env::current_account_id(),
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone(),
            NO_DEPOSIT,
            gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
          )
          .then(
            ext_self_foundation::on_get_account_unstaked_balance_to_withdraw(
              env::current_account_id(),
              NO_DEPOSIT,
              gas::foundation_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW,
            ),
          )
        }
      }
    }

    /// FOUNDATION'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Withdraws the unvested amount from the early termination of the 
    /// vesting schedule.
    pub fn termination_withdraw(&mut self, receiver_id: AccountId) -> Promise {
      self.assert_called_by_foundation();

      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid."
      );

      require!(
        self.get_termination_status() == Some(TerminationStatus::ReadyToWithdraw),
        "Termination status (Funds) is not ready to withdraw"
      );

      let amount = cmp::min(
        self.get_terminated_unvested_balance().0,
        self.get_account_balance().0
      );

      require!(
        amount > 0,
        "Insufficient liquid balance (amount) to withdraw."
      );

      env::log_str(
        format!(
          "Termination Step: Withdrawing {} of terminated unvested balance to @{}",
          amount, receiver_id
        )
        .as_str(),
      );

      self.set_termination_status(TerminationStatus::WithdrawingFromAccountInProgress);

      Promise::new(receiver_id.clone()).transfer(amount).then(
        ext_self_foundation::on_withdraw_unvested_amount(
          amount.into(),
          receiver_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::foundation_callbacks::ON_WITHDRAW_UNVESTED_AMOUNT,
        ),
      )
    }
}



To terminate and wtihdraw, we need to check for several things. Example staking: we need to pass the number of epoch before the funds are ready to withdraw, hence the termination status needs to be checked against, and receiver needs to be the correct person (same ID whom put the fund in in the first place).

We also want to check for something to withdraw before withdrawing.

Here is the gas callback again:

use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}

Now if you ask me, how did we manage to know what to write if we're given a blank .rs file? No we don't actually know the code, so we'll do try and see, just like how you write program usually. This section doesn't do that, it directly tells you what is being done. (In a later section we might deal with something from a blank slate and try and see).

Some changes to near-sdk 4.0 pre-release

Previously for cross-contract calls, we are passing in a reference (with &, example &env::something), now it changes to use env::something. But this also means for the &self.staking_information.as_ref().unwrap().staking_pool_account_id in the original code won't work, as after removing & we want them to implement the Copy trait, which is not available. Hence, we can only do .clone() at the end as a temporary workaround to this issue. Keep in mind we might want a different method of dealing with this as .clone() is said to be (one of?) the most inefficient manner to deal with objects. That will have to wait for the super developers to determine how to deal with them when near-sdk 4.0 becomes stable.

Gas

Another thing is Gas. There are some errors that looks like this:

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants
  --> src/gas.rs:20:30
   |
20 |     pub const UNSTAKE: Gas = super::BASE_GAS * 3;
   |                              ^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0015`.

which means we can't create a constant directly like this (which results in the error above):

pub const SOME_CONST: Gas = super::BASE_GAS * 3;

As a current workaround, it's something more complicated, we have to force it be constant ourselves until this is fixed. To force it:

use near_sdk::Gas;

/// For local processing and local updates. 
const BASE_GAS: Gas = Gas(25_000_000_000_000);


pub mod whitelist {
    use near_sdk::Gas;

    /// Gas attached to the promise checking whether the given
    /// staking pool account ID is whitelisted. 
    pub const IS_WHITELISTED: Gas = super::BASE_GAS;
}


pub mod staking_pool {
    use near_sdk::Gas;

    /// Gas attached to deposit call on staking pool contract.
    /// Local updates + restaking (potentially)
    pub const DEPOSIT: Gas = Gas(super::BASE_GAS.0 * 2);

    /// Gas attached to deposit call on staking pool contract.
    /// local updates + staking call (2x Base).
    pub const DEPOSIT_AND_STAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas attached to withdraw call on staking pool contract. 
    /// BASE for execution + 2 * BASE for transferring amount and 
    /// potentially restaking. 
    pub const WITHDRAW: Gas = Gas(super::BASE_GAS.0 * 3);


    /// Gas attached to unstake call on staking pool contract. 
    /// BASE for execution + 2 * BASE for staking call. 
    pub const UNSTAKE: Gas = Gas(super::BASE_GAS.0 * 3);

    /// Gas to unstake all call on staking pool contract.
    /// BASE for execution + 2 * BASE for staking call.
    pub const UNSTAKE_ALL: Gas = Gas(super::BASE_GAS.0 * 3);


    /// The amount of Gas required to get the current staked balance
    /// of this account from staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_STAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of Gas required to get current unstaked balance of 
    /// this account from the staking pool. 
    /// Requires BASE for local processing. 
    pub const GET_ACCOUNT_UNSTAKED_BALANCE: Gas = super::BASE_GAS;

    /// The amount of gas required to get the current total balance
    /// of this account from the staking pool.
    pub const GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to stake call on staking pool contract.
    /// Execution + staking call (2x)
    pub const STAKE: Gas = Gas(super::BASE_GAS.0 * 3);
}


pub mod owner_callbacks {
    use near_sdk::Gas;

    /// Gas attached to inner callback for processing whitelist
    /// check results. 
    pub const ON_WHITELIST_IS_WHITELISTED: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of 
    /// deposit call to staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT: Gas = super::BASE_GAS;

    /// Gas attached to inner callbacks for processing result of 
    /// the deposit and stake call to the staking pool. 
    pub const ON_STAKING_POOL_DEPOSIT_AND_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result of
    /// the call to get the current total balance from the staking
    /// pool. 
    pub const ON_GET_ACCOUNT_TOTAL_BALANCE: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the 
    /// withdraw call to the staking pool. 
    pub const ON_STAKING_POOL_WITHDRAW: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the
    /// call to get the current unstaked balance from staking pool.
    /// Callback might proceed with withdrawing this amount. 
    /// local updates + withdrawal + another callback
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER: Gas = 
      Gas(
        super::BASE_GAS.0
        + super::staking_pool::WITHDRAW.0
        + ON_STAKING_POOL_WITHDRAW.0
      );

    /// Gas attached to the inner callback for processing result
    /// of the stake call to the staking pool. 
    pub const ON_STAKING_POOL_STAKE: Gas = super::BASE_GAS;

    /// Gas attached to the inner callback for processing result 
    /// of the unstake call to the staking pool.
    pub const ON_STAKING_POOL_UNSTAKE: Gas = super::BASE_GAS;

    /// Gas to unstake all call to staking pool.
    pub const ON_STAKING_POOL_UNSTAKE_ALL: Gas = super::BASE_GAS;

    /// Gas attached to the inner callbacks for checking result for
    /// transfer voting call to voting contract.
    pub const ON_VOTING_GET_RESULT: Gas = super::BASE_GAS;
}


pub mod foundation_callbacks {
    use near_sdk::Gas;


    /// Gas attached to inner callback for processing result of the call to get
    /// the current staked balance from staking pool. 
    /// The callback might proceed without unstaking. 
    pub const ON_GET_ACCOUNT_STAKED_BALANCE_TO_UNSTAKE: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::UNSTAKE.0 
      + ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION.0);


    /// Gas attached to inner callback for processing result of unstake call to
    /// staking pool. 
    pub const ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION: Gas = super::BASE_GAS;

    /// Gas attached to inner callback for processing result of the call to get
    /// the current unstaked balance from staking pool. 
    /// The callback might proceed with withdrawing this amount. 
    pub const ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW: Gas = Gas(
      super::BASE_GAS.0
      + super::staking_pool::WITHDRAW.0
      + ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION.0);

    // Gas attached to inner callback for processing result of withdraw call.
    pub const ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION: Gas = super::BASE_GAS;
    

    /// Gas attached to inner callback for processing result of withdrawal
    /// of terminated unvested balance. 
    pub const ON_WITHDRAW_UNVESTED_AMOUNT: Gas = super::BASE_GAS;
}


pub mod transfer_poll {
    use near_sdk::Gas;

    /// Gas attached to check whether transfers were enabled on transfer poll
    /// contract. 
    pub const GET_RESULT: Gas = super::BASE_GAS;
}

This does not work either, resulting in same error:

pub const SOME_CONST: Gas = Gas(
  (super::BASE_GAS * 3).0
) 

Next, let's take a look at the lib.rs file for the external cross-contract calls ext_ that we skipped earlier.

#TODO: (test might be included later)

Later noted

There's a termination_prepare_to_withdraw where the ext_staking_pool::get_account_staked_balance we have the env::current_account_id() as second argument instead of first. Same for ext_staking_pool::get_account_unstaked_balance. We got it wrong; it should be first argument. If you don't make it first, if you did the integration tests yourself, you'll get this error:

---- test_termination_with_staking_hashed::termination_with_staking_hashed stdout ----
thread 'test_termination_with_staking_hashed::termination_with_staking_hashed' panicked at 'Outcome ExecutionOutcome {
    logs: [],
    receipt_ids: [
        `CzcTYusrPhZMzY4idvaxnDdeqD27q2tFCVC49RVBems2`,
    ],
    burnt_gas: 2513996987497,
    tokens_burnt: 251399698749700000000,
    status: Failure(Action #0: Smart contract panicked: Callback computation 0 was not successful),
} was a failure', /home/azureuser/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-sim-4.0.0-pre.4/src/outcome.rs:90:9
stack backtrace:
   0: rust_begin_unwind
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b/library/std/src/panicking.rs:498:5
   1: core::panicking::panic_fmt
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b/library/core/src/panicking.rs:107:14
   2: near_sdk_sim::outcome::ExecutionResult::assert_success
             at /home/azureuser/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-sim-4.0.0-pre.4/src/outcome.rs:90:9
   3: sim::test_termination_with_staking_hashed::termination_with_staking_hashed
             at ./tests/sim/test_termination_with_staking_hashed.rs:213:5
   4: sim::test_termination_with_staking_hashed::termination_with_staking_hashed::{{closure}}
             at ./tests/sim/test_termination_with_staking_hashed.rs:5:1
   5: core::ops::function::FnOnce::call_once
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b/library/core/src/ops/function.rs:227:5
   6: core::ops::function::FnOnce::call_once
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

with a backtrace. Because it's a compiled wasm, Smart contract panicked: Callback computation 0 was not successful is just not very helpful.

References

Cross-contract calls in main library

Let's take a look at lib.rs. The first resourceful code line is this:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

When we have a contract compiled as Wasm file, it has a certain amount of file size. The larger the file size is, the more NEAR it takes to store it on chain. Keep in mind that storing data on-chain is super duper expensive. Based on the near-sdk-rs docs, the maximum upload is \( 4 \) MB transaction size limit (see the last paragraph, as of this article written). Second, you wouldn't want your wasm file to even reach more than 100KB or you're going to pay a fortune to store them on chain. As of writing, my testnet account uses \( 208 \) kB of storage on-chain (check Near Explorer for your storage used), which cost \( 2.08285 \) $NEAR (about USD \( 23.79 \) at current rate) (check your wallet, and click on Account tab).

So, we know that this contract, after compilation, requires less than \( 3.5 \) NEAR for storage (\( 3.5 \) $NEAR is Max, else you need to raise this value if it's not enough to cover storage requested).

Next, we'll take a look at the cross contract calls. Let's start with communication with the staking-pool contract.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

One thing that crossed my mind, that is untested, is the type it returns. One presume you need to match the type it return with the external contract. Example, WrappedBalance is returned here, so perhaps the function in the external contract requires to also return WrappedBalance rather than Balance? (One isn't sure, you can try it and find out and comment in the discussion page).

And of course, you need to have the respective function at the other contract to add them here, or it would panic with unable to find the function.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

Then there are several other contracts included, above shows the whitelist contract and the Transfer Poll contract (one can't find a reference for this sorry).

Then there are the special ext_self contract here that cross-contract call to itself. These are used for callbacks, like during cleanups when it needs to call itself in the future after everything's done. Since cross-contract call uses Promises and are in the future, it can call itself "in-cross". 😎

Here, we have two of them, one called by the owner contract, one by the foundation contract, hence ext_self_owner and ext_self_foundation respectively. Particularly since we look at foundation.rs before this, the ext_self_foundation here will explains the arguments we pass in before.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

There's something called #[callback] on the on_whitelist_is_whitelisted arguments, that's a callback. If you check the "Whitelist Example" of near-sdk-rs docs, it'll explain to you what this function is (look at the final few paragraphs and the final code block for the equivalence code with and without using the #[callback] macro).

Particularly, those decorated with #[callback] macro won't be found passing in arguments explicitly when we call .then(). That's because the argument is only available during callbacks, which will be passed in by the system (that is, by near_sdk::env). Check again, as I described in the previous paragraph, the last code block for the equivalent if you don't do this (how you call it via env::promise_result(0) for example).

There may be some names that are similar, like on_get_account_unstaked_balance_to_withdraw in foundation, adding *_by_owner to the end for owner's, so make sure to not confuse them by keeping them in mind that they're calling to a different "sub-contract".

With that, we now fill in those cross-contract calls that previously does not exist in foundation.rs.

Let's move on to the next page for the rest of lib.rs, particularly on the lib.rs implementation of LockupContract.

References

lib.rs implementation of LockupContract

We shall take a look at the main implementation of LockupContract.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

We have some arguments passed in here. If you remember, we met these argument details in types.rs, so you might consider referring back there for specific information.

Actually, we only have a single function new to create a new LockupContract here. The other implementations are more specific to say Foundation or Owner, so you could find the other implementation functions in their respective .rs file (and hence their respective pages).

There's a part that mentioned transfer:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

This means that, if current transfer is disabled TransfersDisabled, then it is "not unlocked", so we need to verify AccountID's validity. However if it's TransfersEnabled, it's already verified somewhere else that it's valid (or how does it transfer?), hence we don't need to verify anymore.

Based on this code:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

We can understand that, first, we need a vesting schedule. Only after we have a vesting schedule, we add (and require to add) foundation account to the vesting schedule. We can add foundation account first as there's no vesting schedule. So there's the arrangement.

Skipped

Currently we skipped the unit test (more like simulation tests actually than unit test) and come back to it later on.

Next, we'll look at some tests for foundation.rs before moving to foundation callbacks and finish up with the contract concerning NEAR Foundation.

References

Test for Foundation (Tentative?)

We will write some local test to foundations. To first start, let's know that some of the test utilities are put inside src/tests/test_utils.rs which can be imported. This allows not to cramp everything inside the actual testing module.

We won't be demonstrating this, but you can visit the page here.

One originally want to separate this into the foundation.rs file but found it couldn't be imported in any ways unless create the same file multiple times and put them in different folders, which is just redundant. Hopefully Rust can change this in the future as it makes no sense to import from different test utils if you really pull it out to share them across multiple files; but for now, we'll have to put everything in the lib.rs one afraid.

Anyway, let's take a look at some of the tests related to foundation.rs. We shall start with the test related to terminate_vesting.

Usually when we do test, we try not to write too much tests, else if we change a small thing then the test also changes, which is redundant and unsustainable. But if there is a need to perform unit test on a module, then we can try to group test together; as in some test run multiple functions, and these related functions could be tested together.

Here, terminate_vesting itself can test lots of stuffs, like test for self.assert_called_by_foundation() (though one suggests this as a separate test if it's always being used), test for require!, test for correct output if everything goes correctly, sometimes test for edge cases. From the smart contract, it makes sense that tests should cover two category:

  1. Output is expected. We don't want to have a user transfer 10 NEAR to account B but account B only receive 5 NEAR and the rest burnt unexpectedly.
  2. Security. The assertions are in place to confirm for security. Hence, our test may be designed from the point of view of hackers and check whether our contract panic when somebody tries to hack through the contract.

Of course, we aren't God, hence we won't be able to cover everything. We try to cover as much as possible what our team and the public can think of based on suggestions by others or your own thoughts and put those that are critical in.

Another thing one wants to say is, while the signer_account_id and predecessor_account_id in near_sdk are no longer just pure String (which previously pub type AccountId = String is used in v3.1.0), the near-sdk::VMContext haven't change to reflect that change, so we need to convert our AccountId back .to_string() as a temporary workaround. One is doing this rather than just returning pure String to ease understanding, though of course one agree that this is more redundant than just returning String from functions in terms of optimization purposes.

Another change that exist is context.is_view. You won't see it in the first 3 tests (you'll see it in the last text). Basically when you set this to false, execution will not charge gas fee while false, and continue charging after you change it back to true.

Explaining a bit on the =4.0.0-pre.5 change.

First, the change on current_account_id now taking in AccountId. But bear in mind, one still uses =4.0.0-pre.4 because of this issue. The AccountId here it takes in isn't near-sdk::AccountId, but near-account-id::AccountId (and it has to be near-account-id-0.10.0 version (don't use 0.12.0 version or it says near-account-id::AccountId is not equal near-account-id::AccountId when the words reads the same, but difference comes in version)). So we can't use the latest version just yet until they port the correct AccountId in.

Next is the is_view, which is now a Method, hence you cannot assign value to it; it's read only. However, after reading the docs, we found something similar. This puts the execution in view mode and defines its configuration. The assignment is different here, as it now takes in not a bool but an Option<ViewConfig>. Let's explain.

First, ViewConfig is from near-primitives-core, so you need to change your toml file to import that library:

[dependencies]
near-primitives-core = "0.12.0"

(you could do anything above 0.10.0, as of writing).

Then you can look at the docs and find how to import it, we see that it contains a max_gas_burnt field, which I won't speak what this means so that you can take your effort and look it up yourself.

Finish looking up? Great. Let's continue.

If the max_gas_burnt field is defined, or perhaps it doesn't have to be defined, one don't know at this point (we'll come back to it later), if we have Some(ViewConfig {}) (is this valid?) or Some(ViewConfig { max_gas_burnt: 10u128.pow(12) }), then it's similar to config.is_view = true. Luckily for the basic_context() that we're going to use later, we just need this to be None, so we don't need to care at this point how it works out.

That's it, let's continue.

Continue on with our test cases

Let's know that we did some shortcuts to remove redundant lines of code:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

So the first test, we shall test calling by non-foundation and expect it to fail.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

Then we see that we passed in a vesting_schedule_with_salt, and we want to test and see if we change the value inside, it'll fail as we expected it to. Particularly, it'll fail if we use a different salt, and it'll fail if we have a different vesting schedule.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

Then we'll skip some test like testing for no vesting_schedule_with_salt provided.

Note that we use assert rather than require here. For consistency we could continue to use require; but it is not required. require is a lightweight version of assert and it's useful during deployment, but during testing, you can have heavyweight (well, it's actually not that heavyweight, just that on-chain requires computation that cost gas fee so it's best to decrease this as much as possible) macro.

Next, let's look at termination with staking. This is one of the longest assertion as it is more like a simulation test than normal test. As we know, with staking, we will get "deficit" as staking gets us rewards, hence account balance will increase to more than when we stake (minus gas fee and storage fee). We want to simulate staking and ensure it works.

(The original staking contract might be too long and we might reduce it to smaller tests for easier explanation; but in real, you just write one single test as seen here in line 1728 onwards as of writing (or search for test_termination_with_staking())).

The first part is the setup:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

context.is_view based on the docs, it says the execution should not charge any costs while it's true.

As of writing this section, we commented out two of the test here (contract.get_owners_balance() and contract.get_liquid_owners_balance()) that should be there to assert the initialization is correct to make things simpler (the functions are getters which depends on other getters which depends on other getters...). You however might not be seeing they covered up because we're writing a single contract, and when we have the function we might unleash them.

The others, as the test mentions, check that we have the correct unvested and vested amount. Those numbers are determined by the vested schedule, and should be such and such at such and such timing.

Next is select staking pool and deposit to staking pool. As usual, we'll skip quite a lot of tests that should be there to make things simpler. (We might not even include those as comments here, so make sure to check back the original code and find out).

There's another thing one wants to test, which are the rest of the foundation, but really they requires the select_staking_pool or it'll fail with staking pool not selected; hence we can only test the fail case not the not-failed case until we worked through the owner.rs. We'll come back to it later.

The failed case are as below. We'll abandon this test for proper testing later in future pages. I'll put the rest of this code here (after changing the name too)!

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

With that, let's move on to Foundation callbacks.

Foundation Callbacks

While you're reading this line, one uses some time to fill in those lacked assertions and functions in foundation.rs, as one mentioned one won't be explaining them (but they still have to be there for it to run). Let's go through quickly what are these functions and where they go into so you can lookup for it yourself from the LockUp Contract.

  • assert_vesting in internal.rs.
  • get_unvested_amount in getters.rs.
  • assert_staking_pool_is_idle in internal.rs.
  • get_termination_status in getters.rs.
  • set_termination_status in internal.rs.
  • set_staking_pool_status in internal.rs.
  • get_terminated_unvested_balance in getters.rs.

So most assertions and setters are in internal.rs and getters in getters.rs (of course).

There's one thing one didn't mentioned before. For the original contract, internal.rs uses pub for their functions, but one thought they're internal, so one changes all of them to pub(crate) just in case the pub is needed, but we don't access it outside the crate.

With these, we're ready to move on to the next part, the foundation_callbacks.rs.

Again, these are still inside the implementation of LockupContract, which we sub-divided them into sections for easier understanding. These are the callback methods for the NEAR Foundation sub-contracts.

The first function gets the account staked balance to unstake that much balance. It's a callback, so we have the #[callback] tag on the argument; which means it calls the env::promise_result(0) given that env::promise_results_count() equals to 1 (hence only single result, and 0 means fetch the 0th element, just like how you fetch elements from an array). Then, the PromiseResult that is returned is assigned to val, whether it being Successful, or Failed will give different value. Note that one hypothesize this might ignored the NotReady trait, since val is a bool and it can only save 2 values, so it makes sense to save the Successful and Failed trait. For more information, again refer to this article.

use crate::*;

use near_sdk::{near_bindgen, PromiseOrValue, assert_self,
  is_promise_success};
use std::convert::Into;


#[near_bindgen]
impl LockupContract {
    /// Called after the request to get the current staking balance to unstake 
    /// everything for vesting schedule termination.
    pub fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if staked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{}",
            staked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::unstake(
          staked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::UNSTAKE,
        )
        .then(
          ext_self_foundation::on_staking_pool_unstake_for_termination(
            staked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION,
          ),
        ).into()
      } else {
        env::log_str("Terminating: Nothing to unstake.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} succeeded",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      } else {
        self.set_termination_status(TerminationStatus::VestingTerminatedWithDeficit);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} has failed",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      unstake_succeeded
    }

    
    /// Called after the request to get the current unstaked balance to 
    /// withdraw everything from vesting schedule termination. 
    pub fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        )
        .then(
          ext_self_foundation::on_staking_pool_withdraw_for_termination(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION,
          ),
        ).into()

      } else {
        env::log_str(
          concat!(
            "Terminating: Nothing to withdraw from staking pool. ", 
            "They had been withdrawn to account, which you can withdraw from to ",
            "your account."
          )
        );
          
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_withdraw_for_termination(
      &mut self, 
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw); 

        {
          let staking_information = self.staking_information.as_mut().unwrap();

          // Due to staking rewards, deposit can be negative. 
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount.0.saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} succeeded.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

      } else {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} failed.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after the foundation tried to withdraw the terminated unvested
    /// balance.
    pub fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();

      if withdraw_succeeded {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} succeeded.",
            amount.0, receiver_id
          ).as_str(),
        );

        // Decreasing lockup amount after withdrawal.
        self.lockup_information.termination_withdrawn_tokens += amount.0;
        let unvested_amount = self.get_terminated_unvested_balance().0;

        if unvested_amount > amount.0 {
          // There is still unvested balance remaining. 
          let remaining_balance = unvested_amount - amount.0;

          self.vesting_information = 
              VestingInformation::Terminating(TerminationInformation {
                unvested_amount: remaining_balance.into(),
                status: TerminationStatus::ReadyToWithdraw,
              });

          env::log_str(
            format!(
              "Terminating: {} still awaits withdrawal",
              remaining_balance
            ).as_str(),
          );

          if self.get_account_balance().0 == 0 {
            env::log_str(
              concat!("The withdrawal is completed: no more balance can be ",
              "withdrawn in a future call.")
            );
          }

        } else {
          self.foundation_account_id = None;
          self.vesting_information = VestingInformation::None;
          env::log_str("Vesting schedule termination and withdrawal completed.");
        }

      } else {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} FAILED.",
            amount.0, receiver_id,
          ).as_str(),
        );
      }

      withdraw_succeeded
    }
}

If staked_balance is positive, we have something we can unstake, otherwise we'll just say nothing to unstake and move on. If there is something to unstake, we'll perform cross-contract calls with the staking pool contract, asking them to unstake whatever we have, then we'll perform (a series of) callbacks to clean up the calls. To emphasize, callbacks can be considered the last function call of a series of function calls to clear up everything that needs to be clean up, and finalizes the function call. This is performed via a Promise, which is the contract calling on itself (a cross-contract call that itself call on itself) for cleaning up.

Here, we have this itself as a callback, calling another callback in the .then(), which is a function called on_staking_pool_unstake_for_termination. We'll look at this next (since this function is in the same Rust file as the function you had just seen).

use crate::*;

use near_sdk::{near_bindgen, PromiseOrValue, assert_self,
  is_promise_success};
use std::convert::Into;


#[near_bindgen]
impl LockupContract {
    /// Called after the request to get the current staking balance to unstake 
    /// everything for vesting schedule termination.
    pub fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if staked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{}",
            staked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::unstake(
          staked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::UNSTAKE,
        )
        .then(
          ext_self_foundation::on_staking_pool_unstake_for_termination(
            staked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION,
          ),
        ).into()
      } else {
        env::log_str("Terminating: Nothing to unstake.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} succeeded",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      } else {
        self.set_termination_status(TerminationStatus::VestingTerminatedWithDeficit);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} has failed",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      unstake_succeeded
    }

    
    /// Called after the request to get the current unstaked balance to 
    /// withdraw everything from vesting schedule termination. 
    pub fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        )
        .then(
          ext_self_foundation::on_staking_pool_withdraw_for_termination(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION,
          ),
        ).into()

      } else {
        env::log_str(
          concat!(
            "Terminating: Nothing to withdraw from staking pool. ", 
            "They had been withdrawn to account, which you can withdraw from to ",
            "your account."
          )
        );
          
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_withdraw_for_termination(
      &mut self, 
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw); 

        {
          let staking_information = self.staking_information.as_mut().unwrap();

          // Due to staking rewards, deposit can be negative. 
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount.0.saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} succeeded.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

      } else {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} failed.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after the foundation tried to withdraw the terminated unvested
    /// balance.
    pub fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();

      if withdraw_succeeded {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} succeeded.",
            amount.0, receiver_id
          ).as_str(),
        );

        // Decreasing lockup amount after withdrawal.
        self.lockup_information.termination_withdrawn_tokens += amount.0;
        let unvested_amount = self.get_terminated_unvested_balance().0;

        if unvested_amount > amount.0 {
          // There is still unvested balance remaining. 
          let remaining_balance = unvested_amount - amount.0;

          self.vesting_information = 
              VestingInformation::Terminating(TerminationInformation {
                unvested_amount: remaining_balance.into(),
                status: TerminationStatus::ReadyToWithdraw,
              });

          env::log_str(
            format!(
              "Terminating: {} still awaits withdrawal",
              remaining_balance
            ).as_str(),
          );

          if self.get_account_balance().0 == 0 {
            env::log_str(
              concat!("The withdrawal is completed: no more balance can be ",
              "withdrawn in a future call.")
            );
          }

        } else {
          self.foundation_account_id = None;
          self.vesting_information = VestingInformation::None;
          env::log_str("Vesting schedule termination and withdrawal completed.");
        }

      } else {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} FAILED.",
            amount.0, receiver_id,
          ).as_str(),
        );
      }

      withdraw_succeeded
    }
}

The first thing you might be wondering, why are we repeating the part in env::log_str except for the log message; why can't we have a function that stores these information and when we call it with tuple unpacking to unpack the information than repeating the code twice?

Well, you can of course. The original code is written separately because they don't want to use variables, as variables means extra computation and storage; and on-chain, that means extra gas fee. So if you could confirm by minimizing the code repetition it doesn't affect the gas fee (even in the slightest bit), go ahead with it. If it does affect gas fee, then you need to think of tradeoffs. If you tradeoff 100 functions, these gas fee adds up. Do you have enough gas to perform the function, and are you willing to pay for the function call if you're going to call it a billion times in the future, adding the fee up? And from a programmers' perspective, is your code easy to read (also important though not related to previous questions)?

Now, we're going to move on to the second part. The previous two functions are responsible for unstaking the staked balance; while the next two functions are responsible for withdrawing the unstaked balance. Just like the first function calling the second function as a promise; the next two function will have its first function (the third function overall) calling the second function (the fourth function overall) with a Promise. The logic flow are about similar, just trying to do different work.

use crate::*;

use near_sdk::{near_bindgen, PromiseOrValue, assert_self,
  is_promise_success};
use std::convert::Into;


#[near_bindgen]
impl LockupContract {
    /// Called after the request to get the current staking balance to unstake 
    /// everything for vesting schedule termination.
    pub fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if staked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{}",
            staked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::unstake(
          staked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::UNSTAKE,
        )
        .then(
          ext_self_foundation::on_staking_pool_unstake_for_termination(
            staked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION,
          ),
        ).into()
      } else {
        env::log_str("Terminating: Nothing to unstake.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} succeeded",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      } else {
        self.set_termination_status(TerminationStatus::VestingTerminatedWithDeficit);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} has failed",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      unstake_succeeded
    }

    
    /// Called after the request to get the current unstaked balance to 
    /// withdraw everything from vesting schedule termination. 
    pub fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        )
        .then(
          ext_self_foundation::on_staking_pool_withdraw_for_termination(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION,
          ),
        ).into()

      } else {
        env::log_str(
          concat!(
            "Terminating: Nothing to withdraw from staking pool. ", 
            "They had been withdrawn to account, which you can withdraw from to ",
            "your account."
          )
        );
          
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_withdraw_for_termination(
      &mut self, 
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw); 

        {
          let staking_information = self.staking_information.as_mut().unwrap();

          // Due to staking rewards, deposit can be negative. 
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount.0.saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} succeeded.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

      } else {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} failed.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after the foundation tried to withdraw the terminated unvested
    /// balance.
    pub fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();

      if withdraw_succeeded {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} succeeded.",
            amount.0, receiver_id
          ).as_str(),
        );

        // Decreasing lockup amount after withdrawal.
        self.lockup_information.termination_withdrawn_tokens += amount.0;
        let unvested_amount = self.get_terminated_unvested_balance().0;

        if unvested_amount > amount.0 {
          // There is still unvested balance remaining. 
          let remaining_balance = unvested_amount - amount.0;

          self.vesting_information = 
              VestingInformation::Terminating(TerminationInformation {
                unvested_amount: remaining_balance.into(),
                status: TerminationStatus::ReadyToWithdraw,
              });

          env::log_str(
            format!(
              "Terminating: {} still awaits withdrawal",
              remaining_balance
            ).as_str(),
          );

          if self.get_account_balance().0 == 0 {
            env::log_str(
              concat!("The withdrawal is completed: no more balance can be ",
              "withdrawn in a future call.")
            );
          }

        } else {
          self.foundation_account_id = None;
          self.vesting_information = VestingInformation::None;
          env::log_str("Vesting schedule termination and withdrawal completed.");
        }

      } else {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} FAILED.",
            amount.0, receiver_id,
          ).as_str(),
        );
      }

      withdraw_succeeded
    }
}
use crate::*;

use near_sdk::{near_bindgen, PromiseOrValue, assert_self,
  is_promise_success};
use std::convert::Into;


#[near_bindgen]
impl LockupContract {
    /// Called after the request to get the current staking balance to unstake 
    /// everything for vesting schedule termination.
    pub fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if staked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{}",
            staked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::unstake(
          staked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::UNSTAKE,
        )
        .then(
          ext_self_foundation::on_staking_pool_unstake_for_termination(
            staked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION,
          ),
        ).into()
      } else {
        env::log_str("Terminating: Nothing to unstake.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} succeeded",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      } else {
        self.set_termination_status(TerminationStatus::VestingTerminatedWithDeficit);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} has failed",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      unstake_succeeded
    }

    
    /// Called after the request to get the current unstaked balance to 
    /// withdraw everything from vesting schedule termination. 
    pub fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        )
        .then(
          ext_self_foundation::on_staking_pool_withdraw_for_termination(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION,
          ),
        ).into()

      } else {
        env::log_str(
          concat!(
            "Terminating: Nothing to withdraw from staking pool. ", 
            "They had been withdrawn to account, which you can withdraw from to ",
            "your account."
          )
        );
          
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_withdraw_for_termination(
      &mut self, 
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw); 

        {
          let staking_information = self.staking_information.as_mut().unwrap();

          // Due to staking rewards, deposit can be negative. 
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount.0.saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} succeeded.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

      } else {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} failed.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after the foundation tried to withdraw the terminated unvested
    /// balance.
    pub fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();

      if withdraw_succeeded {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} succeeded.",
            amount.0, receiver_id
          ).as_str(),
        );

        // Decreasing lockup amount after withdrawal.
        self.lockup_information.termination_withdrawn_tokens += amount.0;
        let unvested_amount = self.get_terminated_unvested_balance().0;

        if unvested_amount > amount.0 {
          // There is still unvested balance remaining. 
          let remaining_balance = unvested_amount - amount.0;

          self.vesting_information = 
              VestingInformation::Terminating(TerminationInformation {
                unvested_amount: remaining_balance.into(),
                status: TerminationStatus::ReadyToWithdraw,
              });

          env::log_str(
            format!(
              "Terminating: {} still awaits withdrawal",
              remaining_balance
            ).as_str(),
          );

          if self.get_account_balance().0 == 0 {
            env::log_str(
              concat!("The withdrawal is completed: no more balance can be ",
              "withdrawn in a future call.")
            );
          }

        } else {
          self.foundation_account_id = None;
          self.vesting_information = VestingInformation::None;
          env::log_str("Vesting schedule termination and withdrawal completed.");
        }

      } else {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} FAILED.",
            amount.0, receiver_id,
          ).as_str(),
        );
      }

      withdraw_succeeded
    }
}

And I have no idea why there is a curly braces around the if withdraw_succeeded block.

Lastly, we have the callbacks for termination_withdraw function. This callback will be called when withdrawing unvested amount.

use crate::*;

use near_sdk::{near_bindgen, PromiseOrValue, assert_self,
  is_promise_success};
use std::convert::Into;


#[near_bindgen]
impl LockupContract {
    /// Called after the request to get the current staking balance to unstake 
    /// everything for vesting schedule termination.
    pub fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if staked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{}",
            staked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::unstake(
          staked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::UNSTAKE,
        )
        .then(
          ext_self_foundation::on_staking_pool_unstake_for_termination(
            staked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_UNSTAKE_FOR_TERMINATION,
          ),
        ).into()
      } else {
        env::log_str("Terminating: Nothing to unstake.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} succeeded",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      } else {
        self.set_termination_status(TerminationStatus::VestingTerminatedWithDeficit);
        env::log_str(
          format!(
            "Terminating: Unstaking {} from @{} has failed",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      unstake_succeeded
    }

    
    /// Called after the request to get the current unstaked balance to 
    /// withdraw everything from vesting schedule termination. 
    pub fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        )
        .then(
          ext_self_foundation::on_staking_pool_withdraw_for_termination(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::foundation_callbacks::ON_STAKING_POOL_WITHDRAW_FOR_TERMINATION,
          ),
        ).into()

      } else {
        env::log_str(
          concat!(
            "Terminating: Nothing to withdraw from staking pool. ", 
            "They had been withdrawn to account, which you can withdraw from to ",
            "your account."
          )
        );
          
        self.set_staking_pool_status(TransactionStatus::Idle);
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        PromiseOrValue::Value(true)
      }
    }

    /// Called after the given amount is unstaked from the staking pool contract
    /// due to vesting termination. 
    pub fn on_staking_pool_withdraw_for_termination(
      &mut self, 
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw); 

        {
          let staking_information = self.staking_information.as_mut().unwrap();

          // Due to staking rewards, deposit can be negative. 
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount.0.saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} succeeded.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );

      } else {
        self.set_termination_status(TerminationStatus::EverythingUnstaked);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} from @{} failed.",
            amount.0,
            self.staking_information
                .as_ref()
                .unwrap()
                .staking_pool_account_id
                .clone()
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after the foundation tried to withdraw the terminated unvested
    /// balance.
    pub fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();

      if withdraw_succeeded {
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} succeeded.",
            amount.0, receiver_id
          ).as_str(),
        );

        // Decreasing lockup amount after withdrawal.
        self.lockup_information.termination_withdrawn_tokens += amount.0;
        let unvested_amount = self.get_terminated_unvested_balance().0;

        if unvested_amount > amount.0 {
          // There is still unvested balance remaining. 
          let remaining_balance = unvested_amount - amount.0;

          self.vesting_information = 
              VestingInformation::Terminating(TerminationInformation {
                unvested_amount: remaining_balance.into(),
                status: TerminationStatus::ReadyToWithdraw,
              });

          env::log_str(
            format!(
              "Terminating: {} still awaits withdrawal",
              remaining_balance
            ).as_str(),
          );

          if self.get_account_balance().0 == 0 {
            env::log_str(
              concat!("The withdrawal is completed: no more balance can be ",
              "withdrawn in a future call.")
            );
          }

        } else {
          self.foundation_account_id = None;
          self.vesting_information = VestingInformation::None;
          env::log_str("Vesting schedule termination and withdrawal completed.");
        }

      } else {
        self.set_termination_status(TerminationStatus::ReadyToWithdraw);
        env::log_str(
          format!(
            "Terminating: Withdrawing {} to @{} FAILED.",
            amount.0, receiver_id,
          ).as_str(),
        );
      }

      withdraw_succeeded
    }
}

Since these foundation callbacks are callbacks, we won't be testing them independently. Rather, our test would start with foundation.rs, which we have done before but cannot continue because the contract is not finished (requirement of owner.rs, which we would be looking at next).

Owner's Methods

We shall took at the methods that can be called by the owner. The owner is the original caller of the contract, at least in this case. For example, Bob wants to stack some NEAR to the staking pool. Bob will be the owner. We shall see later on (in the next page) when transfers no longer refer to the original caller of the contract.

As the owner, of course he can select a staking pool to stake, be it staking with NEAR, staking with Aurora, or staking with Everstake, for example. So that's our first function: select_staking_pool.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

As with every owner's method, we need to assert that these methods can be called only by the owner. Then we can have some other assertions specific to the method. For example, here we need to check that the selected staking pool is a valid staking pool, we need to check that a staking pool has not yet been selected already, and we need to check that there's no termination (of staking) in progress (if you terminated, you might have to wait for finished termination before restaking again).

The contract also needs to check whether the pool you want to stake to is the current whitelisted pool. If it is blacklisted then you cannot stake to the pool; if it is on queue, perhaps you have to wait for it to be current validators before you can stake, one don't know. If it's paused, one don't know.

While you're reading this, one fill in the unfilled assertions and gas methods. As usual, you can check the assertions from internal.rs yourself on how it works, but that's not totally required (but one encourages you to check it out yourself to know what is being compared to raise assertions). The name itself explains a lot. You can also check the gas methods in gas.rs to see how much gas is used.

We also see that checking for whitelist is done in the future using a callback (a Promise).

With the ability to select a staking pool, owner can unselect the selected staking pool!

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

Before we move on, there is a repeated code block of assertions in the upcoming few functions, hence we moved them out into its own function for DRY.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

Perhaps the developer think that repeating might save gas fee, or it doesn't work with repeating? We don't know until we find out. Luckily, we have some test cases written by the original developer that we could copy them and see if they still passes and determine the problem!

assert_staking_pool_is_idle will check that the staking pool contract is currently idle (not working on another stuff) before performing cross-contract call. You can imagine this: there are multiple caller that could stake with the validator. This is a many-to-one relationship: one staking pool contract serving multiple owner's contract. Hence you need this assertion.

Continue with our owner's method, you also see the comments that the contract doesn't care about leftovers. When you try to unstake things, there are just some value that are too small (perhaps few yoctoNEARs, but may be larger value; though compare to overall, they're too many decimal places that the contract think it's too small to care about). So if you want to continue accumulate these until it round off to a large enough value, you can. If you don't want to, you can just throw it away and unstake now. It's up to the owner (which may be you if you're the one staking) to make his/her choice.

Selecting and unselecting staking pool isn't very useful it you can deposit NEAR to it, and potentially staking on it.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

Note the above function is deposit to staking pool an amount, not staking on it, just pure deposit an amount to the pool. We shall see a deposit_and_stake next that deposits + stake.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

The logic didn't really lies in the above function; instead, they're in the staking pool contract. Visit the Staking Pool Contract to find out more. The above function mostly is including the required checks before passing the request on to staking pool contract to handle it (and internal callbacks to clean up).

Next is a function that checks the liquid balance available in the pool. This is useful when owner earn rewards and want to withdraw these rewards (plus originally staked balance). This contract query the pool (it does not makes decision whether it has enough balance or not for the owner to withdraw; that is for another function).

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

Of course, we check so we can withdraw, hence we requires the withdrawal function.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

We can choose not to specify an amount and withdraw everything. Most code are similar except it calls a different cross-contract function.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

In particular, since we don't enter an amount, it will query for the unstaked balance your account has on the staking pool to prepare for withdrawal.

Up till now, we only have functions to deposit NEAR to the staking pool, but we didn't have any function to stake NEAR yet. Let's do that now.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

It mentioned "given extra amount", which just means anything that you haven't staked. It doesn't mean the rewards you earn per se! The rewards you earn are counted, but those that you deposit to the staking pool are also "extra amount" before you stake them.

And of course, to unstake them from the selected staking pool. Since we select staking pool first before choosing to stake/unstake, we don't need to pass in "stake_pool" as arguments to the function. If you aren't sure how this works, try to stake some amount (perhaps 0.1 NEAR) on a staking pool on NEAR wallet and play with it. You'll see that you first need to select the pool, then only you can say to stake or unstake on that pool with a specified amount (DON'T put MAX, because you need to pay Gas fee to withdraw later. Leave a small amount on your wallet depending on how much you stake. If you stake a lot, withdrawing might require more gas fee so you need more in your wallet.)

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

You might have noticed one thing, we keep repeating this block of code, which means even if we call "deposit", "stake" we need to run this block of code 4 times (and that's just visible here, what says the Promises it called might also need the information):

self.staking_information
    .as_ref()
    .unwrap()
    .staking_pool_account_id
    .clone()

We could actually take it out and call it, that's not a problem. If you're caching the result within the "stake", "unstake", "deposit" block locally, fine. They might take up some Gas fee to store the variable, but that's not a big problem and might even save some computation time (which we didn't do here, and one suggest you to try it, but you need to check whether passing a variable instead of the actual code above to the Promise results the same or not). A problem is when we try to cache the result.

If you cache the result, we never know whether what we query may change after subsequent function calls, hence it's important that you never cache the result globally, only locally within the function. This is for security reasons, and might cause severe problems if you don't follow this advice. You might be depositing to a pool, in the middle the id changes for whatever reason, and continuing with the cache result would either panic the contract in the best case scenario (where your money could be returned with proper callbacks), or transferred to other staking pool or even get lost in the worse case scenario.

We will skip the unstake_all here, you can refer to it in the References link, and CTRL+F your way to the function name.

Next, we look at check_transfers_vote. Note previously we mentioned owner is no longer the caller at some point, here it is. Turn to next page for more information.

References

Contract as the owner

Previously, we mentioned owner is the caller of the contract. Well, there are some misconceptions that one hide previously. So let's see what it is, and let's clarify what one means "caller of the contract". This is what you previously taught (or thought) it was:

original_idea

But look, we never mentioned anything about the lockup contract here, so where is it? It's actually what I mean by "the owner". So the updated definition is:

altered_idea

The reason it's called owner, it's not because "you" called it, but because the "lockup contract" called the "staking pool contract", hence the lockup contract is the owner here. To make it easy to understand, I refer the owner as you the caller. It's true in a way for staking, as you're the one calling the lockup contract which in turns call the staking pool contract. We are looking at the owner is you + lockup contract as an entity; now we split it up, we see it more clearly refers to the lockup contract.

This is important because the next function, check_transfers_vote, is called by the contract, not by you. The lockup contract wants to check whether transfers are enabled by voting, but it has nothing to do with end users except for the DAO that did the vote (if it is enabled by voting). Otherwise, the transfer date might have been set at the beginning of the vesting period when it'll start transfer.

You might still have query, but one must say that one don't understand how it works fully either. One also don't know what it means by transfer; as in, who is the receiver? One don't know. Tell me in the discussions, open up a new discussion, if you know more about this and explain to me.

One shall put the rest of the code here therefore, without specific explanation on its purposes.

use crate::*;
use near_sdk::{near_bindgen, AccountId, Promise, PublicKey, require};

#[near_bindgen]
impl LockupContract {
    fn repeated_assertions(&mut self) {
      self.assert_owner();
      self.assert_staking_pool_is_idle();
      self.assert_no_termination();
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Select staking pool contract at the given account ID. The staking pool
    /// first has to be checked against staking pool whitelist contract. 
    pub fn select_staking_pool(
      &mut self, 
      staking_pool_account_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(staking_pool_account_id.as_bytes()),
        "The staking pool account ID is invalid"
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      env::log_str(
        format!(
          "Selecting staking pool @{}. Checking if this pool is whitelisted.",
          staking_pool_account_id
        ).as_str(),
      );

      ext_whitelist::is_whitelisted(
        staking_pool_account_id.clone(),
        self.staking_pool_whitelist_account_id.clone(),
        NO_DEPOSIT,
        gas::whitelist::IS_WHITELISTED,
      )
      .then(
        ext_self_owner::on_whitelist_is_whitelisted(
          staking_pool_account_id,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_WHITELIST_IS_WHITELISTED,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 25 TGas
    /// 
    /// Unselects the current(ly selected) staking pool.
    /// It requires that there are no known deposits left on
    /// the currently selected staking pool.
    pub fn unselect_staking_pool(&mut self) {
      // self.assert_owner();
      // self.assert_staking_pool_is_idle();
      // self.assert_no_termination();
      self.repeated_assertions();

      // This is best effort check. There may still be leftovers
      // in the staking pool. Owner can choose to or not to 
      // unselect. The contract doesn't care about leftovers. 
      require!(
        self.staking_information.as_ref().unwrap()
            .deposit_amount.0 == 0,
        "There is still deposit on staking pool."
      );

      env::log_str(
        format!(
          "Unselecting current staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.staking_information = None;
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 100 TGas
    /// 
    /// Deposits the given extra amount to the staking pool.
    pub fn deposit_to_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Deposit amount should be positive."
      );

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Deposits and stakes the given extra amount to the 
    /// selected staking pool.
    pub fn deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(amount.0 > 0, "Deposit amount should be positive.");

      require!(
        self.get_account_balance().0 >= amount.0,
        "Trying to stake more than you have is not possible."
      );

      env::log_str(
        format!(
          "Depositing and staking {} to @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::deposit_and_stake(
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        amount.0,
        gas::staking_pool::DEPOSIT_AND_STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_deposit_and_stake(
          amount, 
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_DEPOSIT_AND_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 75 TGas
    /// 
    /// Retrieves total balance from staking pool and save it
    /// internally. Useful when owner wants to receive rewards
    /// during unstaking for querying total balance in the pool
    /// that could be withdrawn. 
    pub fn refresh_staking_pool_balance(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Fetching total balance from staking pool @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_total_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_TOTAL_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_total_balance(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_TOTAL_BALANCE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Withdraws the given amount from the staking pool.
    pub fn withdraw_from_staking_pool(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Withdrawing {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::withdraw(
        amount,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::WITHDRAW,
      )
      .then(
        ext_self_owner::on_staking_pool_withdraw(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 175 TGas
    /// 
    /// Tries to withdraw all unstaked balance from staking pool.
    pub fn withdraw_all_from_staking_pool(&mut self) -> Promise {
      self.repeated_assertions();

      env::log_str(
        format!(
          "Querying unstaked balance on @{} to withdraw everything.",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::get_account_unstaked_balance(
        env::current_account_id(),
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::GET_ACCOUNT_UNSTAKED_BALANCE,
      )
      .then(
        ext_self_owner::on_get_account_unstaked_balance_to_withdraw_by_owner(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_GET_ACCOUNT_UNSTAKED_BALANCE_TO_WITHDRAW_BY_OWNER,
        ),
      )
    }

    /// ONWER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Stakes the given extra amount at the staking pool.
    pub fn stake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive."
      );

      env::log_str(
        format!(
          "Staking {} at @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::stake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::STAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_stake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_STAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes the given amount at the staking pool.
    pub fn unstake(
      &mut self,
      amount: WrappedBalance
    ) -> Promise {
      self.repeated_assertions();
      require!(
        amount.0 > 0,
        "Amount should be positive"
      );

      env::log_str(
        format!(
          "Unstaking {} from @{}",
          amount.0,
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake(
        amount, 
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE,
      )
      .then(
        ext_self_owner::on_staking_pool_unstake(
          amount,
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE,
        )
      )
    }

    /// OWNER'S METHOD
    /// 
    /// Requires 125 TGas
    /// 
    /// Unstakes all tokens from staking pool
    pub fn unstake_all(&mut self) -> Promise {
      self.repeated_assertions();
      
      env::log_str(
        format!(
          "Unstaking all tokens from @{}",
          self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone()
        ).as_str(),
      );

      self.set_staking_pool_status(TransactionStatus::Busy);

      ext_staking_pool::unstake_all(
        self.staking_information
              .as_ref()
              .unwrap()
              .staking_pool_account_id
              .clone(),
        NO_DEPOSIT,
        gas::staking_pool::UNSTAKE_ALL,
      ).then(
        ext_self_owner::on_staking_pool_unstake_all(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_STAKING_POOL_UNSTAKE_ALL,
        )
      )
    }

    /// Requires 75 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner.
    /// 
    /// Calls voting contract to validate if the transfers were enabled by 
    /// voting. Once transfers are enabled, they can't be disabled anymore. 
    pub fn check_transfers_vote(&mut self) -> Promise {
      self.assert_owner();
      self.assert_transfers_disabled();
      self.assert_no_termination();

      let transfer_poll_account_id = 
          match &self.lockup_information.transfers_information
      {
        TransfersInformation::TransfersDisabled {
          transfer_poll_account_id
        } => transfer_poll_account_id,
        _ => unreachable!(),
      };

      env::log_str(
        format!(
          "Checking that transfers are enabled at the transfer poll contract @{}",
          &transfer_poll_account_id.clone()
        ).as_str(),
      );

      ext_transfer_poll::get_result(
        transfer_poll_account_id.clone(),
        NO_DEPOSIT,
        gas::transfer_poll::GET_RESULT,
      )
      .then(
        ext_self_owner::on_get_result_from_transfer_poll(
          env::current_account_id(),
          NO_DEPOSIT,
          gas::owner_callbacks::ON_VOTING_GET_RESULT,
        )
      )
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else 
    /// except the owner.
    /// 
    /// Transfers the given amount to the given receiver 
    /// account ID. This requires transfers to be enabled
    /// within the voting contract.
    pub fn transfer(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId
    ) -> Promise {
      self.assert_owner();
      require!(
        env::is_valid_account_id(receiver_id.as_bytes()),
        "The receiver account ID is invalid"
      );
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_liquid_owners_balance().0 >= amount.0,
        format!(
          concat!(
            "The available liquid balance {} is smaller than ",
            "the requested tranfer amount {}"),
          self.get_liquid_owners_balance().0,
          amount.0,
        ),
      );

      env::log_str(
        format!(
          "Transferring {} to @{}",
          amount.0,
          receiver_id
        ).as_str()
      );

      Promise::new(receiver_id).transfer(amount.0)
    }


    /// Requires 50 TGas
    /// Not intended to hand over the access to someone else
    /// except the owner. 
    /// 
    /// Adds full access key with the given public key to the 
    /// account. You need:
    /// - Fully-vested account
    /// - Lockup duration expired.
    /// - Transfers enabled.
    /// - No termination in progress (either none or must be finished)
    /// This account will become a regular account, contract will be removed.
    pub fn add_full_access_key(
      &mut self,
      new_public_key: PublicKey
    ) -> Promise {
      self.assert_owner();
      self.assert_transfers_enabled();
      self.assert_no_staking_or_idle();
      self.assert_no_termination();

      require!(
        self.get_locked_amount().0 == 0,
        "Tokens are still locked/unvested"
      );

      env::log_str("Adding a full access key");

      // We pass in PublicKey, not Base58PublicKey, so no need this. 
      // let new_public_key: PublicKey = new_public_key.into();

      Promise::new(
        env::current_account_id()
      ).add_full_access_key(new_public_key)
    }
}

There's also a final function that changes the contract account from only accessible via a function key to be accessible via a full access key. This function have to be careful when created; if hacker gets access to this function, the whole staking pool is confiscated.

Next, we shall made some of the callbacks.

Owner's Callbacks

Here we put down the code for callbacks. Unless necessary, we won't discuss them in depth. As usual callbacks, you have necessary assertions. These callbacks, unlike the ones in foundation_callbacks.rs, don't return Promises. Instead, most of these return bool and most are checks on whether something is true or not before returning with the boolean to the main function in owner.rs.

In fact, we would skip some of these functions not necessary for discussion here. You can refer to the actual ones and fill it in, via the link in the references below.

As usual, they have a #[callback] argument passed in to replace the return value from near_sdk::env as we stated previously.

Note that some of the functions doesn't have a false return. It only returns true. Alternatively, it can panic if it doesn't meet any of the assertions.

If you check on how to optimize for wasm contract (link to LNC website when it became available), reducing the use of format! and prefer &str helps. However, when one tries to take it out from env::log_str into its own function like this,

impl LockupContract {
    fn logging_string(string: &str, amount: u128) -> &str {
      format!(
        string,
        amount.0,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone()
      ).as_str()
    }

    ...
}

It has errors:

error: format argument must be a string literal
  --> src/owner_callbacks.rs:12:9
   |
12 |         string,
   |         ^^^^^^
   |
help: you might be missing a string literal to format with
   |
12 |         "{} {} {}", string,
   |         +++++++++++

So we can't actually take it out, and alternatively, you could just create a function that is read-only that returns the staking_pool_account_id; one isn't sure if the result returned will be the same (theoretically one hypothesizes it will). You can try, for sure, with some in-code assertions to check before deployment.

If you really need, certainly you can use a const for &str with this crate and use either concatcp! to replace concat! or formatcp! to replace format!.

We also decide to have this function taken out into internal.rs so we don't need to repeat it everytime.

use near_sdk::require;
use crate::*;

/*********************
 * Internal Methods
 *********************/

impl LockupContract {
    /// Balance excluding storage staking balance. 
    /// Storage staking balance can't be transferred out
    /// without deleting this contract.
    pub(crate) fn get_account_balance(&self) -> WrappedBalance {
      env::account_balance()
          .saturating_sub(MIN_BALANCE_FOR_STORAGE)
          .into()
    }

    pub(crate) fn set_staking_pool_status(
      &mut self,
      status: TransactionStatus
    ) {
      self.staking_information
          .as_mut()
          .expect("Staking pool should be selected.")
          .status = status;
    }

    pub(crate) fn set_termination_status(
      &mut self,
      status: TerminationStatus
    ) {
      if let VestingInformation::Terminating(termination_information) = 
          &mut self.vesting_information 
      {
        termination_information.status = status;
      } else {
        unreachable!("Vesting information not at terminating stage. ");
      }
    }

    pub(crate) fn assert_vesting(
      &self,
      vesting_schedule_with_salt: Option<VestingScheduleWithSalt>,
    ) -> VestingSchedule {
      match &self.vesting_information {

        VestingInformation::VestingHash(hash) => {
          if let Some(vesting_schedule_with_salt) = vesting_schedule_with_salt {
            require!(
              &vesting_schedule_with_salt.hash() == &hash.0,
              "Presented vesting schedule and salt don't match the hash."
            );
            vesting_schedule_with_salt.vesting_schedule
          } else {
            env::panic_str("Expected vesting schedule and salt, but not provided.")
          }
        }

        VestingInformation::VestingSchedule(vesting_schedule) => {
          require!(
            vesting_schedule_with_salt.is_none(),
            "Explicit vesting schedule already exists."
          );
          vesting_schedule.clone()
        }

        VestingInformation::Terminating(_) => env::panic_str("Vesting was terminated."),

        VestingInformation::None => env::panic_str("Vesting is None."),
      }
    }

    pub(crate) fn assert_staking_pool_is_idle(&self) {
      require!(
        self.staking_information.is_some(),
        "Staking pool is not selected."
      );

      match self.staking_information.as_ref().unwrap().status {
        
        TransactionStatus::Idle => (),

        TransactionStatus::Busy => {
          env::panic_str("Contract currently busy with another operation. ")
        }
      };
    }

    pub(crate) fn assert_staking_pool_is_not_selected(&self) {
      require!(
        self.staking_information.is_none(),
        "Staking pool is already selected"
      );
    }

    pub(crate) fn assert_no_termination(&self) {
      if let VestingInformation::Terminating(_) = &self.vesting_information {
        env::panic_str(
          "All operations are blocked until vesting termination is completed." 
        );
      }
    }



    pub(crate) fn assert_called_by_foundation(&self) {
      if let Some(foundation_account_id) = &self.foundation_account_id {
        require!(
          &env::predecessor_account_id() == foundation_account_id,
          "Can only be called by NEAR Foundation"
        )
      } else {
        env::panic_str("No NEAR Foundation account specified in the contract.");
      }
    }


    pub(crate) fn assert_owner(&self) {
      require!(
        &env::predecessor_account_id() == &self.owner_account_id,
        "This method can only be called by the owner. "
      )
    }

    pub(crate) fn assert_transfers_enabled(&self) {
      require!(
        self.are_transfers_enabled(),
        "Transfers are disabled."
      );
    }

    pub(crate) fn assert_transfers_disabled(&self) {
      require!(
        !self.are_transfers_enabled(),
        "Transfers are already enabled."
      );
    }


    pub(crate) fn assert_no_staking_or_idle(&self) {
      if let Some(staking_information) = &self.staking_information {
        match staking_information.status {
          TransactionStatus::Idle => (),
          TransactionStatus::Busy => {
            env::panic_str("Contract is currently busy with another operation.")
          }
        };
      }
    }


    pub(crate) fn staking_pool_account_id_clone(&self) -> AccountId {
      self.staking_information
          .as_ref()
          .unwrap()
          .staking_pool_account_id
          .clone()
    }
}

And we can call it via self.staking_pool_account_id_clone() when required, inside LockupContract.

Unable to not use unwrap

To make the contract lightweight, we prefer to not use unwrap nor expect because it'll cause panic!, which isn't ideal. Instead, prefer to use unwrap_or or unwrap_or_else or other methods like match instead.

However, there is one situation when this couldn't really be changed: when it's on the LHS (left-hand side) of the equation. Ultimately, unwrap_or will assign a value to the LHS if it's an equation/algorithm of the RHS. But if you are to assign to it, then one cannot think of another way to go around it. For example, the last line of this function:

use crate::*;
use near_sdk::{
  near_bindgen, PromiseOrValue, assert_self, 
  is_promise_success, require 
};


#[near_bindgen]
impl LockupContract {

    pub fn on_whitelist_is_whitelisted(
      &mut self,
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId
    ) -> bool {
      assert_self();
      require!(
        is_whitelisted,
        "The given staking pool ID is not whitelisted."
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      self.staking_information = Some(StakingInformation {
        staking_pool_account_id,
        status: TransactionStatus::Idle,
        deposit_amount: 0.into(),
      });

      true
    }

    /// Deposit amount transferred from this account to staking pool.
    /// Required to update staking pool status.
    pub fn on_staking_pool_deposit(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let deposit_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if deposit_succeeded {
        self.staking_information
            .as_mut()
            .unwrap()
            .deposit_amount
            .0 
          += amount.0;
        
        env::log_str(
           format!(
             "Depositing {} to @{} succeeded.",
             amount.0,
             self.staking_pool_account_id_clone(),
           ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Depositing {} to @{} failed.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      deposit_succeeded
    }

    /// Deposit out to staking pool and staked. To update the
    /// staking pool status.
    pub fn on_staking_pool_deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let deposit_and_stake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if deposit_and_stake_succeeded {
        self.staking_information
            .as_mut()
            .unwrap()
            .deposit_amount
            .0
          += amount.0;

        env::log_str(
          format!(
            "Depositing and staking {} to @{} succeeded.",
            amount.0,
            self.staking_pool_account_id_clone(),
            ).as_str(),
         );
 
       } else {
         env::log_str(
           format!(
             "Depositing  and staking{} to @{} failed.",
             amount.0,
             self.staking_pool_account_id_clone(),
           ).as_str(),
         );
      }

      deposit_and_stake_succeeded
    }

    /// Requested given amount transferring from staking pool
    /// to this account. Update staking pool status.
    pub fn on_staking_pool_withdraw(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        {
          let staking_information = self.staking_information
                                        .as_mut()
                                        .unwrap();
          // Deposit can be negative due to rewards
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount
              .0
              .saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Withdrawing {} from @{} succeeded",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Withdrawing {} from @{} failed",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after extra amount stake was staked. 
    pub fn on_staking_pool_stake(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let stake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if stake_succeeded {
        env::log_str(
          format!(
            "Staking {} at @{} succeeded.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Staking {} at @{} failed.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      stake_succeeded
    }

    /// Called after given amount unstaked at staking pool.
    pub fn on_staking_pool_unstake(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        env::log_str(
          format!(
            "Unstaking {} at @{} succeeded.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Unstaking {} at @{} failed.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }
      unstake_succeeded
    }

    /// All tokens unstaked from staking pool.
    pub fn on_staking_pool_unstake_all(&mut self) -> bool {
      assert_self();

      let unstake_all_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_all_succeeded {
        env::log_str(
          format!(
            "Unstake all at @{} succeeded.",
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Unstake all at @{} failed.",
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      unstake_all_succeeded
    }

    /// Called after the transfer voting contract was checked for the vote result.
    pub fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult,
    ) -> bool {
      assert_self();
      self.assert_transfers_disabled();

      if let Some(transfers_timestamp) = poll_result {
        env::log_str(
          format!(
            "Transfers were successfully enabled at {}",
            transfers_timestamp.0
          ).as_str(),
        );

        self.lockup_information
            .transfers_information = TransfersInformation::TransfersEnabled {
              transfers_timestamp,
            };

        true

      } else {
        env::log_str("Transfers not yet enabled.");
        false
      }
    }


    /// Called after the request to get the current total balance from the 
    /// staking pool.
    pub fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    ) {
      assert_self();
      self.set_staking_pool_status(TransactionStatus::Idle);

      env::log_str(
        format!(
          "Staking pool current total balance: {}",
          total_balance.0
        ).as_str(),
      );

      self.staking_information.as_mut().unwrap().deposit_amount = total_balance;
    }

    /// Called after the request to get the current unstaked balance to withdraw
    /// everything by the owner. 
    pub fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        // Withdraw
        env::log_str(
          format!(
            "Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_pool_account_id_clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        ).then(
          ext_self_owner::on_staking_pool_withdraw(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
          )
        ).into()


      } else {
        env::log_str("No unstaked balance to withdraw from staking pool.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        PromiseOrValue::Value(true)
      }
    }
}

Finally

Before we end, one put all the available unit test from the lib.rs of the original repo in. There will be lots of errors, particularly concerning AccountId and how to parse it. One already teaches you about this so you can certainly work it out yourself.

Another problem is the "panic message but expected substring" failure. As one don't agree with some of the panic substring (one prefer shorter English without wasting words), some of the substring don't match. We just need to change and replace the expected substring with what we entered in, in the test, not from the code.

Third, one of the panic message is long, as it panics from the contract. This is something that can't change, because when it panics, it tries to unwrap and have this message for Result or Option; so we just have to replace the long messages in. This of course may be unstable if the panic messages changes in the future again. You just need to figure out whether the panic message is still for the same reason by reading the panic_msg and update it to the newest panic string everytime the panic changes.

Also, when you do the cargo test, certainly there will be warnings about unused imports: you can remove them now, don't leave them in.

Next, we shall look at the final element of this chapter: Simulation testing.

References

Simulation Testing

Simulation testing is such a heavy topic it deserves its own section. Unlike unit testing, simulation tests tries to emulate what really happens on chain with mockups. For our lockup contract, remember we have cross-contract calls to stacking contract, voting contract, and whitelist contract? All these wasm files need to be included to perform simulation testing, not just our lockup contract's wasm file. With simulation testing you can do more than what you can with unit testing; but it's also more complicated.

Don't confuse simulation tests with end-to-end tests though. The end-to-end tests uses the Jest Testing Suite, which is written in Typescript. Currently there's no replacement for this yet; but perhaps in the future we might use sandbox testing. Today we focus on simulation testing and forget about end-to-end test for now.

If you ever include simulation testing, you require to change something with your toml first. First, include "rlib"; second, import near-sdk-sim. Previously we don't include "rlib" because it adds size to cross contract; however, if you do perform simulation testing, forget about optimizing wasm size. You can't have it both ways.

If you can only pick one between bear paw and fish, you gotta pick the best.
-- A Chinese Proverb

These tests exist outside of your usual src folder in Cargo, just like usual Rust simulation testing. You can run these test like how you usually run unit testing with:

cargo test -- --nocapture

or just cargo test.

In this chapter we won't look at multiple simulation tests; we'll just look at how to create simulation tests, and the rest will be for you to understand. We shall leave the link to the code for you in the References section too! Let's start.

References

Setting up the environment

The first thing we need is to compile the contract. If you didn't compile it beforehand to wasm, running cargo test will result in incorrect results.

The second thing is to import necessary wasm to test. We shall put it in the tests/res folder, just to group them together. At the root of your cargo project folder, run:

mkdir tests
mkdir tests/res

cd tests/res

Then download these files from Github:

NOTE: DO NOT USE "WGET". One got stuck in the error for 3 HOURS because when you use WGET, it downloads the HTML of github rather than the wasm file. What's worse, the error is misleading. The error is "PrepareError: Error happened while deserilaizing the module." and you thought it has something to do with your code rather than the wasm file, otherwise how did you execute it successfully? It's only when inspecting wasm and found that it is actually html code when one realizes it's the error.

These shall get you the necessary contract code for simulating smart contract calls.

Let's look at the contract next. We need to create a rust file first, so:

mkdir tests/sim
touch tests/sim/main.rs
touch tests/sim/utils.rs

They put it in tests/spec.rs, but we don't, following the new convention. Our main tests will locate in tests/sim/main.rs, while some utility functions we shall put in tests/sim/utils.rs.

We import some function from our lockup contract. One uses use lockup because my Cargo file has name the "library" as lockup. In the original contract, they named it lockup-contract, so the use lockup_contract.

We also import necessary functions from near_sdk and near_sdk_sim, and some other libraries. Here, we requires the quickcheck libraries and quickcheck macros too. Let's look into Cargo.toml before continuing.

In your Cargo.toml, it should look like this:

[package]
...

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = "=4.0.0-pre.4"
uint = { version = "0.9.3", default-features = false }

[dev-dependencies]
near-sdk-sim = "4.0.0-pre.4"
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"

Be careful though, because the near docs failed to build for near-sdk-sim "4.0.0-pre.4", we don't know everything about it. If things failed and you can't figure out, consider falling back to "3.2.0" version. Otherwise, one sees that the "4.0.0-pre.1" successfully build docs, so perhaps you could try refer to that version and hopefully infer something from there.

To continue, we import more utilities. In main.rs

use lockup::{
  LockupContractContract, TerminationStatus, TransfersInformation, VestingSchedule,
  VestingScheduleOrHash, VestingScheduleWithSalt, WrappedBalance, MIN_BALANCE_FOR_STORAGE
};
use near_sdk::borsh::BorshSerialize;
use near_sdk::json_types::U128;
use near_sdk::serde_json::json;
use near_sdk::{AccountId, Balance};
use near_sdk_sim::runtime::GenesisConfig;
use near_sdk_sim::{deploy, init_simulator, to_yocto, UserAccount, STORAGE_AMOUNT};
use quickcheck_macros::quickcheck;
use std::convert::TryInto;

mod test_staking_with_helpers;
mod test_termination_with_staking_hashed;

pub(crate) mod utils;  // utils require pub-crate
mod test_staking;  // import other test files like this. 

pub(crate) use crate::utils::*;

pub(crate) fn assert_almost_eq_with_max_delta(left: u128, right: u128, max_delta: u128) {
  assert!(
      std::cmp::max(left, right) - std::cmp::min(left, right) <= max_delta,
      "{}",
      format!(
          "Left {} is not even close to Right {} within delta {}",
          left, right, max_delta
      )
  );
}

pub(crate) fn assert_eq_with_gas(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, to_yocto("0.005"));
}

pub(crate) fn assert_yocto_eq(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, 1);
}


near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
  LOCKUP_WASM_BYTES => "res/lockup.wasm",
  STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
  FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
  WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}

Let's look at some initialization function moved to utils.rs. Here, we add the constants, functions, and wasm file imports. Again, mine is lockup.wasm, but yours might be lockup_contract.wasm, so double check that to match the "name" defined in cargo.toml.

build.sh

#!/bin/bash
set -e

export CONTRACT=lockup.wasm
RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/$CONTRACT res/

In utils.rs at the top, include this.

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

Then below that, we initialize our simulator.

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

We call it basic_setup() here. You could also call it init().

Some of the stuffs we don't really know how to solve. For example, they uses .valid_account_id() previously, we don't know what we need here, so we'll just try to use AccountId as ValidAccountId is deprecated, like we usually do in smart contract code. We'll derive the correct answer as we start testing. So, we call account_id() instead.

Another is integer cannot be inferred, so we need to state it clearly. The error is:

error[E0283]: type annotations needed
   --> tests/sim/utils.rs:63:8
    |
63  |         &json!({
    |  ________^
64  | |         "owner_id": foundation,
65  | |         "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
66  | |         "reward_fee_fraction": {
...   |
69  | |         }
70  | |       }).to_string().into_bytes(), 
    | |________^ cannot infer type for type `{integer}`
    |
    = note: multiple `impl`s satisfying `{integer}: Serialize` found in the `serde` crate:
            - impl Serialize for i128;
            - impl Serialize for i16;
            - impl Serialize for i32;
            - impl Serialize for i64;
            and 8 more
    = note: required because of the requirements on the impl of `Serialize` for `&{integer}`
note: required by a bound in `to_value`

Perhaps the safest to use is u128, but let's just try u16 first and we can change later if it fails.

Some imports on main.rs might not reflect on utils.rs, for whatever reason. If so, just move the imports there from main.rs.

If you got this error:

 no rules expected this token in macro call

That's because you accidentally put a comma in the final argument before the close-bracket. Remove the comma should work.

Let's talk about basic_setup()

basic_setup()

The first thing you see is these 3 lines:

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

This introduces the genesis engine; aka "blockchain". You can treat this as the NEAR blockchain, where all the accounts, contract, etc are held on. root is the simulator.

When we next call create_user:

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

This is simulating wallet creation on-chain. When you create a wallet, you need to specify the wallet address ("example.near"), then you need to transfer some funds to it (either to cover storage costs; or more than that). So we create a foundation account and transfer 10000 NEAR to it; and owner has 30 NEAR.

Next, we need to deploy the whitelist account. It has an initialization function that needs to run once after deployment. On-chain, we do this by deploy, and separately call the function. We could also do that here:

let _whitelist = root.deploy(
  &WHITELIST_WASM_BYTES,
  STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
  to_yocto("30")
);

foundation.call(
  STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
  "new",
  &!json({
    "foundation_account_id": foundation.account_id()
  }).to_string().into_bytes(),
  MAX_GAS,
  NO_DEPOSIT,
).assert_success();

But since this is so common, there is a deploy_and_init function for it, just to reduce code typing.

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

The rest is now easy to understand. foundation call the add_staking_pool so we can add the staking pool to whitelist; which we then can deploy the staking pool. Staking pool also have an initialization function we call at the start. We return everything back.

There is a quickcheck function; but unfortunately it doesn't run on near-sdk-sim v4.0.0-pre.4. It does run on v3.2.0 though; but that requires you use near-sdk-rs v3.1.0 also.

Nevertheless, you can view it in line 54-77 in the code (check References).

Next, we shall look at one of the test function.

References

First Integration Test: Staking

We didn't put everything in a single spec.rs file, allowing us to split test into multiple files and importing them into main.rs. If the test is long, we can have one test function per file. Let's create our first test in its own file. In the same directory as utils.rs, make a test_staking.rs.

Make sure you put the macro in the main.rs file for general use.

use lockup::{
  LockupContractContract, TerminationStatus, TransfersInformation, VestingSchedule,
  VestingScheduleOrHash, VestingScheduleWithSalt, WrappedBalance, MIN_BALANCE_FOR_STORAGE
};
use near_sdk::borsh::BorshSerialize;
use near_sdk::json_types::U128;
use near_sdk::serde_json::json;
use near_sdk::{AccountId, Balance};
use near_sdk_sim::runtime::GenesisConfig;
use near_sdk_sim::{deploy, init_simulator, to_yocto, UserAccount, STORAGE_AMOUNT};
use quickcheck_macros::quickcheck;
use std::convert::TryInto;

mod test_staking_with_helpers;
mod test_termination_with_staking_hashed;

pub(crate) mod utils;  // utils require pub-crate
mod test_staking;  // import other test files like this. 

pub(crate) use crate::utils::*;

pub(crate) fn assert_almost_eq_with_max_delta(left: u128, right: u128, max_delta: u128) {
  assert!(
      std::cmp::max(left, right) - std::cmp::min(left, right) <= max_delta,
      "{}",
      format!(
          "Left {} is not even close to Right {} within delta {}",
          left, right, max_delta
      )
  );
}

pub(crate) fn assert_eq_with_gas(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, to_yocto("0.005"));
}

pub(crate) fn assert_yocto_eq(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, 1);
}


near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
  LOCKUP_WASM_BYTES => "res/lockup.wasm",
  STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
  FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
  WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}

Then, we shall define some functions in main.rs that are used in test functions later. Ensure they have pub(crate) so it could be shared across modules.

use lockup::{
  LockupContractContract, TerminationStatus, TransfersInformation, VestingSchedule,
  VestingScheduleOrHash, VestingScheduleWithSalt, WrappedBalance, MIN_BALANCE_FOR_STORAGE
};
use near_sdk::borsh::BorshSerialize;
use near_sdk::json_types::U128;
use near_sdk::serde_json::json;
use near_sdk::{AccountId, Balance};
use near_sdk_sim::runtime::GenesisConfig;
use near_sdk_sim::{deploy, init_simulator, to_yocto, UserAccount, STORAGE_AMOUNT};
use quickcheck_macros::quickcheck;
use std::convert::TryInto;

mod test_staking_with_helpers;
mod test_termination_with_staking_hashed;

pub(crate) mod utils;  // utils require pub-crate
mod test_staking;  // import other test files like this. 

pub(crate) use crate::utils::*;

pub(crate) fn assert_almost_eq_with_max_delta(left: u128, right: u128, max_delta: u128) {
  assert!(
      std::cmp::max(left, right) - std::cmp::min(left, right) <= max_delta,
      "{}",
      format!(
          "Left {} is not even close to Right {} within delta {}",
          left, right, max_delta
      )
  );
}

pub(crate) fn assert_eq_with_gas(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, to_yocto("0.005"));
}

pub(crate) fn assert_yocto_eq(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, 1);
}


near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
  LOCKUP_WASM_BYTES => "res/lockup.wasm",
  STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
  FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
  WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}

The first thing we need to do is to deploy the contract.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

This uses the deploy! macro. The signer_account is root, which is not a good imitation and shows the limitation of simulation testing. In reality, some account will deploy the contract instead of the "genesis" doing it like what is did here.

Another init_method is quite easy to understand: the 8 parameters matches those parameters passed to new. Let's recall:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
// use near_sdk::json_types::Base58PublicKey;
use near_sdk::{AccountId, env, ext_contract, near_bindgen, require};
// use near_account_id::AccountId;

pub use crate::foundation::*;
pub use crate::foundation_callbacks::*;
pub use crate::getters::*;
pub use crate::internal::*;
pub use crate::owner::*;
pub use crate::owner_callbacks::*;
pub use crate::types::*;

pub mod foundation;
pub mod foundation_callbacks;
pub mod gas;
pub mod owner_callbacks;
pub mod types;

pub mod getters;
pub mod internal;
pub mod owner;

// Not needed as of 4.0.0-pre.1. 
// #[global_allocator]
// static ALLOC: near_sdk::wee_alloc::WeeAlloc = near_sdk::wee_alloc::WeeAlloc::INIT;

const NO_DEPOSIT: u128 = 0;

/// At least 3.5 NEAR to avoid being transferred out to cover 
/// contract code storage and some internal state. 
pub const MIN_BALANCE_FOR_STORAGE: u128 = 3_500_000_000_000_000_000_000_000;

#[ext_contract(ext_staking_pool)]
pub trait ExtStakingPool {
    fn get_account_staked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_unstaked_balance(&self, account_id: AccountId) -> WrappedBalance;
    fn get_account_total_balance(&self, account_id: AccountId) -> WrappedBalance;

    fn deposit(&mut self);
    fn deposit_and_stake(&mut self);

    fn withdraw(&mut self, amount: WrappedBalance);

    fn stake(&mut self, amount: WrappedBalance);
    fn unstake(&mut self, amount: WrappedBalance);

    fn unstake_all(&mut self);
}

#[ext_contract(ext_whitelist)]
pub trait ExtStakingPoolWhitelist {
    fn is_whitelisted(&self, staking_pool_account_id: AccountId) -> bool;
}

#[ext_contract(ext_transfer_poll)]
pub trait ExtTransferPoll {
    fn get_result(&self) -> Option<PollResult>;
}

#[ext_contract(ext_self_owner)]
pub trait ExtLockupContractOwner {
    fn on_whitelist_is_whitelisted(
      &mut self, 
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId,
    ) -> bool;

    fn on_staking_pool_deposit(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_deposit_and_stake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_withdraw(&mut self, amount: WrappedBalance) -> bool;
    
    fn on_staking_pool_stake(&mut self, amount: WrappedBalance) -> bool;
    fn on_staking_pool_unstake(&mut self, amount: WrappedBalance) -> bool;

    fn on_staking_pool_unstake_all(&mut self) -> bool;

    fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult
    ) -> bool;

    fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    );

    // don't be confused the one "by foundation". 
    fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );
}

#[ext_contract(ext_self_foundation)]
pub trait ExtLockupContractFoundation {
    fn on_withdraw_unvested_amount(
      &mut self,
      amount: WrappedBalance,
      receiver_id: AccountId,
    ) -> bool;

    fn on_get_account_staked_balance_to_unstake(
      &mut self,
      #[callback] staked_balance: WrappedBalance,
    );

    fn on_staking_pool_unstake_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;

    fn on_get_account_unstaked_balance_to_withdraw(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance,
    );

    fn on_staking_pool_withdraw_for_termination(
      &mut self,
      amount: WrappedBalance
    ) -> bool;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LockupContract {
    pub owner_account_id: AccountId,
    pub lockup_information: LockupInformation,  // schedule and amount
    pub vesting_information: VestingInformation,  // schedule and termination status
    pub staking_pool_whitelist_account_id: AccountId,
    
    // Staking and delegation information. 
    // `Some` means staking information is available, staking pool selected. 
    // `None` means no staking pool selected.
    pub staking_information: Option<StakingInformation>,

    // AccountId of NEAR Foundation, can terminate vesting. 
    pub foundation_account_id: Option<AccountId>,  
}


impl Default for LockupContract {
    fn default() -> Self {
      env::panic_str("The contract is not initialized.");
    }
}

#[near_bindgen]
impl LockupContract {
    /// Requires 25 TGas
    /// 
    /// Initializes the contract.
    /// (args will be skipped here, explained in types.rs.)
    #[init]
    pub fn new(
      owner_account_id: AccountId,
      lockup_duration: WrappedDuration,
      lockup_timestamp: Option<WrappedTimestamp>,
      transfers_information: TransfersInformation,
      vesting_schedule: Option<VestingScheduleOrHash>,
      release_duration: Option<WrappedDuration>,
      staking_pool_whitelist_account_id: AccountId,
      foundation_account_id: Option<AccountId>,
    ) -> Self {
      require!(
        env::is_valid_account_id(owner_account_id.as_bytes()),
        "Invalid owner's account ID."
      );

      require!(
        env::is_valid_account_id(staking_pool_whitelist_account_id.as_bytes()),
        "Invalid staking pool whitelist's account ID."
      );

      if let TransfersInformation::TransfersDisabled {
        transfer_poll_account_id,
      } = &transfers_information {
        require!(
          env::is_valid_account_id(transfer_poll_account_id.as_bytes()),
          "Invalid transfer poll's account ID."
        );
      };

      let lockup_information = LockupInformation {
        lockup_amount: env::account_balance(),
        termination_withdrawn_tokens: 0,
        lockup_duration: lockup_duration.0,
        release_duration: release_duration.map(|d| d.0),
        lockup_timestamp: lockup_timestamp.map(|d| d.0),
        transfers_information,
      };

      let vesting_information = match vesting_schedule {
        None => {
          require!(
            foundation_account_id.is_none(),
            "Foundation account can't be added without vesting schedule."
          );
          VestingInformation::None
        }

        Some(VestingScheduleOrHash::VestingHash(hash)) => {
          VestingInformation::VestingHash(hash)
        },

        Some(VestingScheduleOrHash::VestingSchedule(vs)) => {
          VestingInformation::VestingSchedule(vs)
        }
      };

      require!(
        vesting_information == VestingInformation::None 
          || env::is_valid_account_id(
            foundation_account_id.as_ref().unwrap().as_bytes()
          ),
        concat!(
          "Either no vesting created or ",
          "Foundation account should be added for vesting schedule."
        )
      );

      Self {
        owner_account_id,
        lockup_information,
        vesting_information,
        staking_information: None,
        staking_pool_whitelist_account_id,
        foundation_account_id,
      }
    }
}

#[cfg(all(test, not(target_arch="wasm32")))]
mod tests {
    use super::*;
    // use std::convert::TryInto;

    use near_sdk::{testing_env, PromiseResult, VMContext};
    // use near_sdk::json_types::U128;

    mod test_utils;
    use test_utils::*;

    // pub type AccountId = String;
    const SALT: [u8; 3] = [1, 2, 3];
    const FAKE_SALT: [u8; 4] = [3, 1, 2, 4];
    const VESTING_CONST: u64 = 10;

    fn basic_context() -> VMContext {
        get_context(
          system_account(), 
          to_yocto(LOCKUP_NEAR), 
          0, 
          to_ts(GENESIS_TIME_IN_DAYS), 
          false,
          // None,
        )
    }


    fn new_vesting_schedule(
      offset_in_days: u64
    ) -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(GENESIS_TIME_IN_DAYS - YEAR + offset_in_days).into(),
          cliff_timestamp: to_ts(GENESIS_TIME_IN_DAYS + offset_in_days).into(),
          end_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR * 3 + offset_in_days).into(),
        }
    }


    #[allow(dead_code)]
    fn no_vesting_schedule() -> VestingSchedule {
        VestingSchedule {
          start_timestamp: to_ts(0).into(),
          cliff_timestamp: to_ts(0).into(),
          end_timestamp: to_ts(0).into(),
        }
    }


    fn new_contract_with_lockup_duration(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
      lockup_duration: Duration,
    ) -> LockupContract {
        let lockup_start_information = if transfers_enabled {
          TransfersInformation::TransfersEnabled {
            transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
          }
        } else {
          TransfersInformation::TransfersDisabled {
            transfer_poll_account_id: "transfers".parse().unwrap(),
          }
        };

        let foundation_account_id = if foundation_account {
          Some(account_foundation())
        } else {
          None
        };

        let vesting_schedule = vesting_schedule.map(|vesting_schedule| {
          VestingScheduleOrHash::VestingHash(
            VestingScheduleWithSalt {
              vesting_schedule,
              salt: SALT.to_vec().into(),
            }
            .hash()
            .into(),
          )
        });

        LockupContract::new(
          account_owner(),
          lockup_duration.into(),
          None, 
          lockup_start_information,
          vesting_schedule,
          release_duration,
          "whitelist".parse().unwrap(),
          foundation_account_id,
        )
    }


    fn new_contract(
      transfers_enabled: bool,
      vesting_schedule: Option<VestingSchedule>,
      release_duration: Option<WrappedDuration>,
      foundation_account: bool,
    ) -> LockupContract {
        new_contract_with_lockup_duration(
          transfers_enabled, 
          vesting_schedule, 
          release_duration, 
          foundation_account, 
          to_nanos(YEAR),
        )
    }


    #[allow(dead_code)]
    fn lockup_only_setup() -> (VMContext, LockupContract) {
        let context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, None, false);
        (context, contract)
    }

    fn initialize_context() -> VMContext {
      let context = basic_context();
      testing_env!(context.clone());
      context
    }

    fn factory_initialize_context_contract() -> (VMContext, LockupContract) {
      let context = initialize_context();
      let vesting_schedule = new_vesting_schedule(VESTING_CONST);
      let contract = new_contract(true, Some(vesting_schedule), None, true);
      (context, contract)
    }

    
    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_terminate_vesting_fully_vested() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);

      
      // We changed this. 
      // context.predecessor_account_id = account_foundation().to_string().to_string();
      // to this: 
      context.predecessor_account_id = non_owner().to_string().to_string();

      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting = new_vesting_schedule(VESTING_CONST);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting,
        salt: SALT.to_vec().into(),
      }));
    }


    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_salt() {
      let mut context = initialize_context();
      let vesting_duration = 10;
      let vesting_schedule = new_vesting_schedule(vesting_duration);
      let mut contract = new_contract(true, Some(vesting_schedule), None, true);
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let real_vesting_schedule = new_vesting_schedule(vesting_duration);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: real_vesting_schedule,
        salt: FAKE_SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash.")]
    fn test_different_vesting() {
      let (mut context, mut contract) = factory_initialize_context_contract();
      context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_id = non_owner().to_string().to_string();
      testing_env!(context.clone());

      let fake_vesting_schedule = new_vesting_schedule(25);
      contract.terminate_vesting(Some(VestingScheduleWithSalt {
        vesting_schedule: fake_vesting_schedule,
        salt: SALT.to_vec().into(),
      }))
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected.")]
    fn test_termination_with_staking_without_staking_pool() {
      let lockup_amount = to_yocto(1000);
      let mut context = initialize_context();
      let vesting_schedule = new_vesting_schedule(0);
      let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

      context.is_view = true;
      testing_env!(context.clone());

      // Originally commented out
      // assert_eq!(contract.get_owners_balance().0, to_yocto(0));
      // assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
      // Originally commented out END here

      assert_eq!(contract.get_locked_amount().0, lockup_amount);
      assert_eq!(
        contract.get_unvested_amount(vesting_schedule.clone()).0,
        to_yocto(750)
      );
      assert_eq!(
        contract.get_locked_vested_amount(vesting_schedule.clone()).0,
        to_yocto(250)
      );
      context.is_view = false;

      context.predecessor_account_id = account_owner().to_string().to_string();
      context.signer_account_pk = public_key(1).into_bytes();
      testing_env!(context.clone());

      // Selecting staking pool
      // --skipped--

      context.is_view = false;
      context.predecessor_account_id = account_foundation().to_string().to_string();
      context.signer_account_pk = public_key(2).into_bytes();
      testing_env!(context.clone());
      contract.termination_prepare_to_withdraw();
      assert_eq!(
        contract.get_termination_status(),
        Some(TerminationStatus::UnstakingInProgress)
      );

    }

    // ============================= OTHER TESTS ============================ //
    #[test]
    fn test_lockup_only_basic() {
        let (mut context, contract) = lockup_only_setup();
        // Checking initial values at genesis time
        context.is_view = true;
        testing_env!(context.clone());

        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(LOCKUP_NEAR)
        );

        // Checking values in 1 day after genesis time
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 1);

        assert_eq!(contract.get_owners_balance().0, 0);

        // Checking values next day after lockup timestamp
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());

        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));
    }

    #[test]
    fn test_add_full_access_key() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_vesting_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "Tokens are still locked/unvested")]
    fn test_add_full_access_key_when_lockup_is_not_finished() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(true, None, Some(to_nanos(YEAR).into()), false);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR - 10);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        testing_env!(context.clone());

        contract.add_full_access_key(public_key(4));
    }

    #[test]
    #[should_panic(expected = "This method can only be called by the owner. ")]
    fn test_call_by_non_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.select_staking_pool("staking_pool".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "Presented vesting schedule and salt don't match the hash")]
    fn test_vesting_doesnt_match() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        let not_real_vesting = new_vesting_schedule(100);
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: not_real_vesting,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: HostError(GuestPanic { panic_msg: \"Expected vesting schedule and salt, but not provided.\" })")]
    fn test_vesting_schedule_and_salt_not_provided() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = new_contract(true, Some(vesting_schedule), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Explicit vesting schedule already exists.")]
    fn test_explicit_vesting() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(5);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule,
            salt: SALT.to_vec().into(),
        }));
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, None, true);
    }

    #[test]
    #[should_panic(expected = "Foundation account can't be added without vesting schedule")]
    fn test_init_foundation_key_no_vesting_with_release() {
        let context = basic_context();
        testing_env!(context.clone());
        new_contract(true, None, Some(to_nanos(YEAR).into()), true);
    }

    #[test]
    #[should_panic(expected = "Can only be called by NEAR Foundation")]
    fn test_call_by_non_foundation() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        context.predecessor_account_id = non_owner().to_string();
        context.signer_account_id = non_owner().to_string();
        testing_env!(context.clone());

        contract.terminate_vesting(None);
    }

    #[test]
    #[should_panic(expected = "Transfers are disabled")]
    fn test_transfers_not_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_enable_transfers() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = Some(to_ts(GENESIS_TIME_IN_DAYS + 10).into());
        context.predecessor_account_id = lockup_account().to_string();
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not unlocked yet
        assert_eq!(contract.get_owners_balance().0, 0);
        assert!(contract.are_transfers_enabled());
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 10);
        testing_env!(context.clone());
        // Unlocked yet
        assert_eq!(
            contract.get_owners_balance().0,
            to_yocto(LOCKUP_NEAR).into()
        );

        context.is_view = false;
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.transfer(to_yocto(100).into(), non_owner());
    }

    #[test]
    fn test_check_transfers_vote_false() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let mut contract = new_contract(false, None, None, false);
        context.is_view = true;
        testing_env!(context.clone());
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        contract.check_transfers_vote();

        let poll_result = None;
        // NOTE: Unit tests don't need to read the content of the promise result. So here we don't
        // have to pass serialized result from the transfer poll.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        assert!(!contract.on_get_result_from_transfer_poll(poll_result));

        context.is_view = true;
        testing_env!(context.clone());
        // Not enabled
        assert!(!contract.are_transfers_enabled());
    }

    #[test]
    fn test_lockup_only_transfer_call_by_owner() {
        let (mut context, mut contract) = lockup_only_setup();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        context.is_view = true;
        testing_env!(context.clone());
        assert_almost_eq(contract.get_owners_balance().0, to_yocto(LOCKUP_NEAR));

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(1).try_into().unwrap();
        context.is_view = false;
        testing_env!(context.clone());

        assert_eq!(env::account_balance(), to_yocto(LOCKUP_NEAR));
        contract.transfer(to_yocto(100).into(), non_owner());
        assert_almost_eq(env::account_balance(), to_yocto(LOCKUP_NEAR - 100));
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_is_not_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        let amount = to_yocto(LOCKUP_NEAR - 100);
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
    }

    #[test]
    fn test_staking_pool_success() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_staking_pool_account_id(), Some(staking_pool));
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        // Assuming there are 20 NEAR tokens in rewards. Unstaking.
        let unstake_amount = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unstake(unstake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake(unstake_amount.into());

        // Withdrawing
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.withdraw_from_staking_pool(unstake_amount.into());
        context.account_balance += unstake_amount;

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_withdraw(unstake_amount.into());
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        context.is_view = false;

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
        assert_eq!(contract.get_staking_pool_account_id(), None);
    }

    #[test]
    fn test_staking_pool_refresh_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(LOCKUP_NEAR) - amount);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, amount);
        context.is_view = false;

        // Assuming there are 20 NEAR tokens in rewards. Refreshing balance.
        let total_balance = amount + to_yocto(20);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.refresh_staking_pool_balance();

        // In unit tests, the following call ignores the promise value, because it's passed directly.
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_get_account_total_balance(total_balance.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(20));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(20));
        context.is_view = false;

        // Withdrawing these tokens
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        let transfer_amount = to_yocto(15);
        contract.transfer(transfer_amount.into(), non_owner());
        context.account_balance = env::account_balance();

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_known_deposited_balance().0, total_balance);
        assert_eq!(contract.get_owners_balance().0, to_yocto(5));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(5));
        context.is_view = false;
    }

    // ================================= PART 2 ===================================== //
    #[test]
    #[should_panic(expected = "Staking pool is already selected")]
    fn test_staking_pool_selected_again() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Selecting another staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.select_staking_pool("staking_pool_2".parse().unwrap());
    }

    #[test]
    #[should_panic(expected = "The given staking pool ID is not whitelisted.")]
    fn test_staking_pool_not_whitelisted() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"false".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(false, staking_pool.clone());
    }

    #[test]
    #[should_panic(expected = "Staking pool is not selected")]
    fn test_staking_pool_unselecting_non_selected() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Unselecting staking pool
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    #[should_panic(expected = "There is still deposit on staking pool.")]
    fn test_staking_pool_unselecting_with_deposit() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(amount.into());

        // Unselecting staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.unselect_staking_pool();
    }

    #[test]
    fn test_staking_pool_owner_balance() {
        let (mut context, mut contract) = lockup_only_setup();
        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).try_into().unwrap();
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);

        let lockup_amount = to_yocto(LOCKUP_NEAR);
        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, lockup_amount);
        context.is_view = false;

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let mut total_amount = 0;
        let amount = to_yocto(100);
        for _ in 1..=5 {
            total_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.deposit_to_staking_pool(amount.into());
            context.account_balance = env::account_balance();
            assert_eq!(context.account_balance, lockup_amount - total_amount);

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_deposit(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(contract.get_known_deposited_balance().0, total_amount);
            assert_eq!(contract.get_owners_balance().0, lockup_amount);
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }

        // Withdrawing from the staking_pool. Plus one extra time as a reward
        let mut total_withdrawn_amount = 0;
        for _ in 1..=6 {
            total_withdrawn_amount += amount;
            context.predecessor_account_id = account_owner().to_string();
            testing_env!(context.clone());
            contract.withdraw_from_staking_pool(amount.into());
            context.account_balance += amount;
            assert_eq!(
                context.account_balance,
                lockup_amount - total_amount + total_withdrawn_amount
            );

            context.predecessor_account_id = lockup_account().to_string();
            testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
            contract.on_staking_pool_withdraw(amount.into());
            context.is_view = true;
            testing_env!(context.clone());
            assert_eq!(
                contract.get_known_deposited_balance().0,
                total_amount.saturating_sub(total_withdrawn_amount)
            );
            assert_eq!(
                contract.get_owners_balance().0,
                lockup_amount + total_withdrawn_amount.saturating_sub(total_amount)
            );
            assert_eq!(
                contract.get_liquid_owners_balance().0,
                lockup_amount - total_amount + total_withdrawn_amount - MIN_BALANCE_FOR_STORAGE
            );
            context.is_view = false;
        }
    }

    #[test]
    fn test_lock_timestmap() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfers".parse().unwrap(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert!(!contract.are_transfers_enabled());

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_lock_timestmap_transfer_enabled() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = LockupContract::new(
            account_owner(),
            0.into(),
            Some(to_ts(GENESIS_TIME_IN_DAYS + YEAR).into()),
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS + YEAR / 2).into(),
            },
            None,
            None,
            "whitelist".parse().unwrap(),
            None,
        );

        context.is_view = true;
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            None,
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingSchedule(vesting_schedule.clone())
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(None);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: to_yocto(250).into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);
        assert_eq!(contract.get_vesting_information(), VestingInformation::None);
    }

    #[test]
    fn test_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let contract = new_contract(true, None, Some(to_nanos(4 * YEAR).into()), false);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(no_vesting_schedule()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(1000)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract.get_locked_vested_amount(no_vesting_schedule()).0,
            to_yocto(500)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
    }


    // ================================= PART 3 ==================================== //
    #[test]
    fn test_vesting_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    // Vesting post transfers is not supported by Hash vesting.
    #[test]
    fn test_vesting_post_transfers_and_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR * 2);
        let contract = LockupContract::new(
            account_owner(),
            to_nanos(YEAR).into(),
            None,
            TransfersInformation::TransfersEnabled {
                transfers_timestamp: to_ts(GENESIS_TIME_IN_DAYS).into(),
            },
            Some(VestingScheduleOrHash::VestingSchedule(
                vesting_schedule.clone(),
            )),
            Some(to_nanos(4 * YEAR).into()),
            "whitelist".parse().unwrap(),
            Some(account_foundation()),
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(1000)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(250));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(750));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(500)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 4 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 5 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(1000));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(1000) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(0));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_no_staking_with_release_duration() {
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract_with_lockup_duration(
            true,
            Some(vesting_schedule.clone()),
            Some(to_nanos(4 * YEAR).into()),
            true,
            0,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(1000));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 2 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_locked_amount().0, to_yocto(500));
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(250));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(0)
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id: AccountId = "near".parse().unwrap();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(250).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(500));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(500));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(0)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(0));
        assert_eq!(contract.get_termination_status(), None);

        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + 3 * YEAR);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(750) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(0)
        );
    }

    #[test]
    fn test_termination_before_cliff() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(YEAR);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::VestingHash(
                VestingScheduleWithSalt {
                    vesting_schedule: vesting_schedule.clone(),
                    salt: SALT.to_vec().into(),
                }
                    .hash()
                    .into()
            )
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );

        // Terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_vesting_information(),
            VestingInformation::Terminating(TerminationInformation {
                unvested_amount: lockup_amount.into(),
                status: TerminationStatus::ReadyToWithdraw,
            })
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            lockup_amount
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, lockup_amount);
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, MIN_BALANCE_FOR_STORAGE);

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(
            (lockup_amount - MIN_BALANCE_FOR_STORAGE).into(),
            receiver_id,
        );

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(contract.get_owners_balance().0, 0);
        assert_eq!(contract.get_liquid_owners_balance().0, 0);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(
            contract.get_terminated_unvested_balance().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );
    }

    #[test]
    fn test_termination_with_staking() {
        let lockup_amount = to_yocto(1000);
        let mut context = basic_context();
        testing_env!(context.clone());
        let vesting_schedule = new_vesting_schedule(0);
        let mut contract = new_contract(true, Some(vesting_schedule.clone()), None, true);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        context.is_view = false;

        context.predecessor_account_id = account_owner().to_string();
        context.signer_account_pk = public_key(2).into();
        testing_env!(context.clone());

        // Selecting staking pool
        let staking_pool: AccountId = "staking_pool".parse().unwrap();
        testing_env!(context.clone());
        contract.select_staking_pool(staking_pool.clone());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(b"true".to_vec()),
        );
        contract.on_whitelist_is_whitelisted(true, staking_pool.clone());

        // Deposit to the staking_pool
        let stake_amount = to_yocto(LOCKUP_NEAR - 100);
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.deposit_to_staking_pool(stake_amount.into());
        context.account_balance = env::account_balance();

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_deposit(stake_amount.into());

        // Staking on the staking pool
        context.predecessor_account_id = account_owner().to_string();
        testing_env!(context.clone());
        contract.stake(stake_amount.into());

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_stake(stake_amount.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_known_deposited_balance().0, stake_amount);
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        context.is_view = false;

        // Foundation terminating
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        context.signer_account_pk = public_key(3).into();
        testing_env!(context.clone());
        contract.terminate_vesting(Some(VestingScheduleWithSalt {
            vesting_schedule: vesting_schedule.clone(),
            salt: SALT.to_vec().into(),
        }));

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(0));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract.get_terminated_unvested_balance_deficit().0,
            to_yocto(650) + MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::VestingTerminatedWithDeficit)
        );

        // Proceeding with unstaking from the pool due to termination.
        context.is_view = false;
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::UnstakingInProgress)
        );

        let stake_amount_with_rewards = stake_amount + to_yocto(50);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(format!("{}", stake_amount_with_rewards).into_bytes()),
        );
        contract.on_get_account_staked_balance_to_unstake(stake_amount_with_rewards.into());

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_staking_pool_unstake_for_termination(stake_amount_with_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::EverythingUnstaked)
        );

        // Proceeding with withdrawing from the pool due to termination.
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        contract.termination_prepare_to_withdraw();
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::WithdrawingFromStakingPoolInProgress)
        );

        let withdraw_amount_with_extra_rewards = stake_amount_with_rewards + to_yocto(1);
        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(
            context.clone(),
            PromiseResult::Successful(
                format!("{}", withdraw_amount_with_extra_rewards).into_bytes(),
            ),
        );
        contract
            .on_get_account_unstaked_balance_to_withdraw(withdraw_amount_with_extra_rewards.into());
        context.account_balance += withdraw_amount_with_extra_rewards;

        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract
            .on_staking_pool_withdraw_for_termination(withdraw_amount_with_extra_rewards.into());

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, lockup_amount);
        assert_eq!(
            contract.get_unvested_amount(vesting_schedule.clone()).0,
            to_yocto(750)
        );
        assert_eq!(contract.get_terminated_unvested_balance().0, to_yocto(750));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_known_deposited_balance().0, 0);
        assert_eq!(
            contract.get_termination_status(),
            Some(TerminationStatus::ReadyToWithdraw)
        );

        // Withdrawing
        context.is_view = false;
        context.predecessor_account_id = account_foundation().to_string();
        testing_env!(context.clone());
        let receiver_id = account_foundation();
        contract.termination_withdraw(receiver_id.clone());
        context.account_balance = env::account_balance();
        assert_eq!(context.account_balance, to_yocto(250 + 51));

        context.predecessor_account_id = lockup_account().to_string();
        testing_env_with_promise_results(context.clone(), PromiseResult::Successful(vec![]));
        contract.on_withdraw_unvested_amount(to_yocto(750).into(), receiver_id);

        context.is_view = true;
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_liquid_owners_balance().0, to_yocto(51));
        assert_eq!(contract.get_locked_amount().0, to_yocto(250));
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            to_yocto(250)
        );
        assert_eq!(contract.get_unvested_amount(vesting_schedule.clone()).0, 0);
        assert_eq!(contract.get_terminated_unvested_balance().0, 0);
        assert_eq!(contract.get_terminated_unvested_balance_deficit().0, 0);
        assert_eq!(contract.get_termination_status(), None);

        // Checking the balance becomes unlocked later
        context.block_timestamp = to_ts(GENESIS_TIME_IN_DAYS + YEAR + 1);
        testing_env!(context.clone());
        assert_eq!(contract.get_owners_balance().0, to_yocto(301));
        assert_eq!(
            contract.get_liquid_owners_balance().0,
            to_yocto(301) - MIN_BALANCE_FOR_STORAGE
        );
        assert_eq!(
            contract
                .get_locked_vested_amount(vesting_schedule.clone())
                .0,
            0
        );
        assert_eq!(contract.get_locked_amount().0, 0);
    }
}

Next, we check that the owner currently does not have anything staked on the staking pool.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

The output is also interesting. We have an .view_method_call which just performs something similar to near view call from near-cli. The returned result is a JSON, so we call unwrap_json() to that. Now, it's not necessarily something returned; if there's nothing, then None is returned instead. In reality, the contract will show an error message with SmartContractPanic error.

We move on to selecting the staking pool and check they are selected as we expect them to.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

We use a function_call here, because we're trying to imitate near call. In short, the near-cli version of this function is:

near call $LOCKUP_CONTRACT select_staking_pool '{
  "account_id": "$STAKING_POOL_ACCOUNT_ID"
}' --gas=$MAX_GAS --amount=0 --accountId $OWNER_STAKING_ACCOUNT

assuming we store the values as environment variables already. We only select the staking pool, nothing is deposited yet; we now have a staking_pool_account_id and we can view it with near view $LOCKUP_CONTRACT get_staking_pool_account_id '{}'; but we don't have any balance (as seen in the last line, res.0 == 0).

We shall now make deposits to the staking pool. We will deposit 100 NEAR.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

Previously we mentioned that staking involves two operation: depositing the fund to the corresponding staking account first, before it's being stake. Here, it's also 2 separate function calls. We have deposited to the staking pool, but not yet stake it. We can see our deposit with get_known_deposited_balance view function. We shall stake now:

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

We use a .view call instead of .view_method_call because it's not in the lockup contract. With lockup contract, we can import the function and directly call; but we only have the wasm file of the staking contract which we call from there, so we use view instead to simulate calling near view from the near-cli.

To update the staking pool balance globally, we refresh it. We can simulate the refresh here. Recall that the function is useful when owner wants to receive rewards during unstaking for querying total balance in the pool that could be withdrawn. In reality, this should be "no-op", meaning that the refresh should be done automatically with staking.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

Next, we simulate the rewards, update the staking pool status by "pinging" it, then check that our staked amount is now larger than previously deposited (the final line assertion). The ping action has no arguments, and we don't need to define a &json! just for it: we can use b"" which is a bytes with nothing inside, it'll get parsed as a &[u8] type as arguments.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

We refresh the staking balance again and check for two things:

  • After refresh, the new stake amount is still larger than previously staked.
  • assert_eq_with_gas will check that the actual amount, after minus gas, is still within what's predictable. This ensures that we don't have a really large difference that comes out of nowhere; and the difference can only be due to gas fee charged.
use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

Then, the process of getting the money back to owner account also has a few step. It start with unstaking the balance. For simplicity, we'll unstake everything. In reality, you can only unstake everything; at least as of writing, the NEAR web wallet only supports unstaking everything staked. It might change in the future.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

Given that we have a lot of these view calls and they look the same, we made a small function to deal with that: (actually, it's a closure; or in Python language, a lambda function)

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

We use a closure because:

  • We cannot clone UserAccount. We just want to borrow it for use and return it after.
  • We want to capture environment because of above case.

Only within a closure you can borrow it temporarily. If we have a separate function, we need to pass the owner object back into the main function after use, otherwise it would meet end of life (lifetime ends).

Note for the unstake, we check for both staked_balance and unstaked_balance, passing different &str to the closure.

Remember we need to wait for some time (4 epochs) before we could withdraw. We also increase some random number (40) to block_height in this process. In reality, block_height increases (approximately) every second because NEAR has (approximately) 1 second finality. 1 epoch is about 12-13 hours; so block height should increase much much more than 40.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

Then we unlock and withdraw. In reality, unlocking balance is done automatically; it's just the simulator doesn't have that capability so we manually intervene.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

Finally, we unselect the staking pool. That's it, we're done with a normal staking procedure.

use crate::*;


#[test]
fn staking() {
    let lockup_amount = to_yocto("1000");
    let (root, foundation, owner, staking_pool) = basic_setup();

    let staking_account: AccountId = STAKING_POOL_ACCOUNT_ID.parse().unwrap();
    let lockup_account : AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
        contract: LockupContractContract,
        contract_id: lockup_account,
        bytes: &LOCKUP_WASM_BYTES,
        signer_account: root,
        deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
        gas: MAX_GAS,
        init_method: new(
            owner.account_id.clone(),
            1000000000.into(),
            None,
            TransfersInformation::TransfersDisabled {
                transfer_poll_account_id: "transfer-poll".parse().unwrap(),
            },
            None,
            None,
            STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
            None
        )
    );

    let get_account_balance = |method: &str| -> U128 {
      owner.view(
        STAKING_POOL_ACCOUNT_ID.parse().unwrap(),
        method,
        &json!({
          "account_id": LOCKUP_ACCOUNT_ID.to_string()
        }).to_string().into_bytes(),
      ).unwrap_json()
    };

    let owner_staking_account = &owner;

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);

    // Selecting staking pool
    owner_staking_account.function_call(
      lockup.contract.select_staking_pool(staking_account.clone()),
      MAX_GAS,
      0,
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id() 
    ).unwrap_json();

    assert_eq!(res, Some(STAKING_POOL_ACCOUNT_ID.parse().unwrap()));

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    // Depositing to the staking pool
    let staking_amount = lockup_amount - to_yocto("100");

    owner_staking_account.function_call(
      lockup.contract.deposit_to_staking_pool(U128(staking_amount)),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, staking_amount);

    // Staking on the staking pool
    owner_staking_account.function_call(
      lockup.contract.stake(U128(staking_amount)), 
      MAX_GAS, 
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_yocto_eq(res.0, staking_amount);

    // Refreshing staking balance. Should be NOOP
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_yocto_eq(res.0, staking_amount);

    // Simulating rewards
    foundation.transfer(
      staking_account.clone(),
      to_yocto("10")
    ).assert_success();

    // Pinging the staking pool
    foundation.call(
      staking_account.clone(),
      "ping",
      b"",
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = get_account_balance("get_account_staked_balance");

    let new_stake_amount = res.0;
    assert!(new_stake_amount > staking_amount);

    // Refresh staking balance again
    owner_staking_account.function_call(
      lockup.contract.refresh_staking_pool_balance(),
      MAX_GAS,
      0
    ).assert_success();

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    let new_total_balance = res.0;
    assert!(new_total_balance >= new_stake_amount);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    // Account for gas rewards
    assert_eq_with_gas(
      res.0, 
      new_total_balance - staking_amount
    );

    // Unstaking everything
    let res: bool = owner_staking_account.function_call(
      lockup.contract.unstake(U128(new_stake_amount)),
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = get_account_balance("get_account_staked_balance");

    assert_eq_with_gas(res.0, 0);

    let res: U128 = get_account_balance("get_account_unstaked_balance");

    assert!(res.0 >= new_total_balance);
    
    root.borrow_runtime_mut().cur_block.block_height += 40;
    root.borrow_runtime_mut().cur_block.epoch_height += 4;

    // The standalone runtime doesn't unlock locked balance. Need to manually intervene.
    let mut pool = staking_pool.account().unwrap();
    pool.amount += pool.locked;
    pool.locked = 0;
    staking_pool
        .borrow_runtime_mut()
        .force_account_update(STAKING_POOL_ACCOUNT_ID.parse().unwrap(), &pool);

    // Withdrawing everything from the staking pool
    let res: bool = owner_staking_account.function_call(
      lockup.contract.withdraw_from_staking_pool(U128(new_total_balance)) ,
      MAX_GAS,
      0
    ).unwrap_json();

    assert!(res);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_known_deposited_balance() 
    ).unwrap_json();

    assert_eq!(res.0, 0);

    let res: U128 = owner.view_method_call(
      lockup.contract.get_owners_balance() 
    ).unwrap_json();

    assert_eq_with_gas(res.0, new_stake_amount - staking_amount);

    // Unselecting the staking pool
    owner_staking_account.function_call(
      lockup.contract.unselect_staking_pool(),
      MAX_GAS,
      0
    ).assert_success();

    let res: Option<AccountId> = owner.view_method_call(
      lockup.contract.get_staking_pool_account_id()
    ).unwrap_json();

    assert_eq!(res, None);
}

There are a few more functions which one won't include them here; but you can always refer to them. Link in References.

There might be some function that one missed or left out because it's repeating. You shall add them back in yourself if you found "no method error". Could you guess the function that we missed out? (We do deliberately miss out one function that you can't see but it's hidden inside). (Remember to rebuild after you change the code!)

That ends what we want to speak about for this chapter. In the next few chapters, we would look into developing our own app. Particularly, something to prepare you for Near Certified Developer (NCD) in Learn Near Club (LNC). (Note the NCD from near.university might be more difficult than NCD from LNC; as far as one is aware based on the syllabus; so be prepared. In NCD for LNC, you demo with other people for 60 minutes; but the website in Near University mentioned you demo yourself for 60 minutes in NCD for Near university).

References

Making our own dApp: Explore two contracts

We start off still, by exploring two smart contracts: sign-up contract and the tipping contract. However, the sign-up contract is a failure contract with lots of security loopholes, while the tipping contract is ok-ish; it's not like groundbreaking, but something new to learn.

One believes that a book shouldn't just teach you what's correct, but also teach you to fail. When you do experimentation, you fail lots and lots of time, then only you pass. So one included the sign-up contract here.

As for the tipping contract, we would also discuss some considerations and some failures met during design, while slowly changing our program to something more appropriate for the context.

Signup Smart Contract

As you might already know, when you create a NEAR wallet, you require deposit some funds to first cover transaction costs and storage costs. This costs around 0.1 NEAR. It's similar for Sender wallet: you can create the wallet without allocating any funds, but if you try to deposit a small amount of money (like 0.001N, about 1 cent USD) to that created account, it'll get locked. Until you deposit enough funds for it to cover storage and transactions, you won't see "available balance".

Hence, we want our users to not have to pay when signing up themselves. After all, they requires logging in with their wallet. If they don't have the wallet in the first place, we want to create one for them, and we shall fund their wallets themselves.

There are two ways we could do this: first, the user create their wallet, then email a request for 0.1N transaction send. This sounds difficult, as you require the wait and other reasons; hence it makes more sense to copy something like the Linkdrop app.

If you search online, there are quite many linkdrop apps. What we discuss above is just the easiest to understand contract; and we want that, as the more complicated it gets, the more difficult it's to understand, the more vulnerable it is to security loopholes, and the more code it requires hence larger contract size. Something easier would suffice.

For a complicated contract, see this contract that makes a linkdrop app into 3 separate contract deployed to 3 separate accounts and all this and that that makes life more difficult. Particularly, it treats contract as objects and go inherit stuffs downwards and do things that one don't even understand what is happening; if you do, sure go ahead and understand what it does, especially if you love inheritance (or maybe it's not inheritance?).

The main linkdrop app (for mainnet) is here. Though, it's not the only linkdrop app available for NEAR. There's the near drop app (beta) that allow mass users onboarding (they onboard at NEARCON Alpha with QR codes, mentioned in their website here). Then there's also the NEAR redpacket app that you might want to try (one never used this before, so use at your own risk).

There are some problem with the main linkdrop app, which one won't mention it here: we shall discover it as we move forward in development.

But before moving to the linkdrop app, why not utilize what we already got? A linkdrop app is deployed on testnet (the top-level account), so let's just utilize it. To play with it, let's just go to the link here and try (make sure you're on testnet, which the link should redirect you to) and see how it works.

Linkdrop main page

Press the login button and login. Allow the connection fee, and you should get to the main page. One assumes you already created a testnet wallet here. After login, it should look like this:

Linkdrop login page

Three buttons: "Create New NEAR Drop", "Show Used Drops" and "logout". Logout is easy to understand; the "Show Used Drops" will display "Used Drops" (so a NEAR Drop that had been used by someone else, yourself or whoever you gifted to). if you click on it, it'll change to "Hide Used Drops". If you don't have Drops yet, it'll say "No Used Drops". Otherwise, it will show your used drops here.

Linkdrop Show Used Drops

We could create a new NEAR Drop. Let's try create with 2 NEAR.

Alert new NEAR Drop

It's an alert box, type in the amount of NEAR and click "Ok". It'll ask you whether to not download keypair before funding, you can download it (we shall discuss about it later so it's best you download it for reference; after which you can delete it).

The name of the txt should be "public_key.txt", so it saves as the public key name, assuming it's unique. You're taken to the page to approve transaction (one assume you know what to do here). The page now changes again.

Drops created

You have two more buttons: "Copy Near Wallet Link" and "Use Drop". If you decide to Use the Drop yourself, you can do it. Here, since we want to gift it to others ("signup link, remember, we don't want to use it ourselves"), we shall not touch the "Use Drop" button; just the "Copy Near Wallet Link". Clicking on it shall copy to your clipboard.

Now, paste the link in a new page, and you'll be redirect to a wallet creation page. You can create a new account.

New Account

You know what to do: you created a wallet already. If this is someone else, you might want to offer guidance; but that's another story.

So we now have 1 NEAR. Wait, 1 NEAR? One thought it's 2 NEAR?

Flaw

Apparently, there's a flaw that's only available in testnet. If you do with mainnet, you certainly get 2 NEAR; but not testnet. the smart contract deployed on mainnet is not the same on testnet; the mainnet have fixed the flaw, but not the testnet.

This flaw is caused by something called the ACCESS_KEY_ALLOWANCE which we shall see in the next page.

It's also lucky that we send 2 NEAR: otherwise with 1 NEAR, it'll fail. We discuss the failure next page: but you're welcome to try it out yourself.

However, there's one more thing to see. Assume you're on mainnet so it doesn't eats your NEAR, hence let's try to deal with 0.1 NEAR sending: (first, refresh the page to see that it shows in the Used Drops)

0.1 Drop

If you click ok, it'll show you cannot create such drop.

too small

Apparently, the min amount of drop is ACCESS_KEY_ALLOWANCE, which we'll discuss more when looking at the contract later.

The last thing to discuss is a security issue: Remember we downloaded the thing before, the .txt stuff? Let's open it up:

Inside, you should see a JSON structure containing the publicKey and the secretKey. The secretKey is the key you send to your friend when you "Copy NEAR wallet link" before, which has the link like this: "wallet.testnet.near.org/where the linkdrop contract is deployed/secret key". Try to create it once again and you'll see what one means: the contract is deployed to "testnet" or "near" (testnet or mainnet) respectively.

However, now you hold the secret key, one cannot be sure if the secret key equals the public key. After all, the secret key is generated based on the public key; so it most probably isn't? If it is, there's a security breach with this contract: you may be able to use the near cli to hack whoever you send the wallet to and retrieve the amount. If you have a hacker background/security audit background, be sure to notify on the discussion pages, creating a new discussion, mentioning this security breach whether it exists or not.

In fact, you don't even need to download the keypair: you could see it in your browser opening up F12 and go to "console", then the bottomest should be "USED DROPS > Array(num_of_used_drops)" where you can expand it and get the secret key.

Though, one suspects it doesn't; because when you create a new wallet, this private key is replaced with whatever seed phrase it is newly generated; hence you cannot access anymore the credentials.

Ok, enough of testing this feature: you can test in your own time. We shall move on to the linkdrop contract.

References

The Linkdrop Smart Contract

Let's take a look at the linkdrop smart contract. The first thing we see is the Cargo.toml file; and we see the near-sdk version is "0.9.2"! So expect lots of stuffs to be so old it expires. We shall upgrade it to near-sdk-rs 4.0.0-pre.4 version.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

There's also an import called near_helper, which is a super simple library one wrote for functions that one keeps getting on and on. You can look at it here. Otherwise, you can copy and paste the source code into internal.rs instead. In the next page we shall temporarily divert and look a bit at how to go about with Cargo, if you don't know yet.

The first thing to note is some of the directory changes, so our import directory also changes. Another is there's no more near_sdk::collections::Map exists: it has changed to other choices. For full support, use std::collections::HashMap. We use LookupMap from NEAR Collections though, because if you think about it, we are going to retrieve based on a specific key (account name or public key); and we don't need the extra functionality of iteration offered by UnorderedMap. Because it can't derive(Default), we need to impl Default. For simplicity we used a vector to define the saving key seen in LookupMap::new; the newest practices uses enum to define instead; but since we only have one of this we shall be lazy.

From near v4 onwards, we don't need this line anymore:

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

as it's initialized automatically. Moving on:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

The linkdrop contract only stores the list of available linkdrop public keys and their corresponding balance that's deposited to that link.

Now, comes the problem with ACCESS_KEY_ALLOWANCE as we discussed before: it's 1 NEAR. That means, a single linkdrop need at least 1 NEAR to make the drop; which is just stupid. One just wants to deposit 0.1 NEAR, and that cannot make it.

Apparently, if you use the flaw contract and ACCESS_KEY_ALLOWANCE, if you send 2 NEAR like we did before, your friend you get 1.9 NEAR; Of course, for mainnet, your friend would still get 2 NEAR. Unfortunately, the mainnet contract isn't available on github; only the testnet. One isn't sure where the ACCESS_KEY_ALLOWANCE goes, though. They might have make it as small as possible so actually mainnet also deducts ACCESS_KEY_ALLOWANCE but it's negligible.

What's still available in both testnet and mainnet, is you still need to send at least 1 NEAR. In mainnet, that will work as it don't eats your NEAR; on testnet, when you try to create the account, smart contract will panic. You send 1 NEAR, but it requires \[ \text{ACCESS_KEY_ALLOWANCE} + \text{storage} + \text{transaction} \approx 1.1 \text{NEAR} \].

The cross contract calls: these are contract calling itself.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

Let's look at linkdrop implementation. Note that we removed some of the functions that we don't want. Particularly, these are the functions for the user creating the linkdrop to claim it themselves. We dropped these aiming to reduce contract size.

The first function is the send function. This function is for the link creator to "send funds to a temporary account for lockup until the account is claimed".

#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        format!(
          "Attached deposit {} must be greater than {}",
          env::attached_deposit(),
          ACCESS_KEY_ALLOWANCE,
        ).to_string()
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

}

Two things to look here. We want to eliminate as much format! as possible, as it takes up more space in the compiled wasm file. So, let's use a library and make it a constant.

We add this under Cargo.toml dependencies:

[dependencies]
near_sdk = "=4.0.0-pre.4"
const_format = "0.2.22"

Then we use the formatcp! to create a specific constant for this error message. We shall eliminate printing what the user input as the user should know what they had inputted. The other variable is another constant, so we're good to make the string a constant. However, Rust string concatenation can only happened on owned String, not immutable &str. Trying to go with format! is trying to run against a river current, fighting against to compiler to no avail. Somebody made the library to deal with this, so we just use their success and build upon.

Import the library and make the constant.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

(if your VSCode shows error importing like mine does, just ignore it. As long as compilation is successful, most likely VSCode forgot to look for it; but it won't affect cargo successfully looking for the crates.)

Second, one suspects because we do the - ACCESS_KEY_ALLOWANCE, it causes the error we discussed earlier (discovering less than what we expect to discover). We shall try to minus it out and test if it can success or not. Even if it fails, we can just uncomment out the contract; we know if we minus out it'll pass: it's being used in linkdrop contract anyways.

One wants to show a test here, but just the send function is insufficient. In fact, let's try to create a view function called get_key_balance to ensure it has been inserted and we can create test for it.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

If you haven't already know, a function with &mut self is a change function and those with &self is treated as view functions. If you don't need the &mut self for change function hence you use &self, as of writing, NEAR blockchain will mistreat your change function as view function hence you cannot call it as a change function.

The final function:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

Then, we shall create the test case for this and test it runs well. As usual, we need to setup the first time we test with necessary functions and imports. Since tests aren't included during wasm compilation, we don't need to use require! and all other tricks to reduce contract size.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, Balance,
  Promise, PromiseResult, PublicKey, require, is_promise_success,
};
use near_sdk::collections::LookupMap;

use near_helper::{expect_lightweight};

// use std::collections::HashMap;

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct LinkDrop {
    pub accounts: LookupMap<PublicKey, Balance>,
}

impl Default for LinkDrop {
  fn default() -> Self {
    Self {
      accounts: LookupMap::new(b"d".to_vec())
    }
  }
}


/// Access key allowance for linkdrop keys: 1 NEAR.
const ACCESS_KEY_ALLOWANCE: u128 = 1_000_000_000_000_000_000_000_000;

/// Gas attached to the callback from account creation: 20 TGas.
pub const ON_CREATE_ACCOUNT_CALLBACK_GAS: u64 = 20_000_000_000_000;

const NO_DEPOSIT: u128 = 0;

use const_format::formatcp;

const ERR_MSG_SEND: &str = formatcp!(
  "Attached deposit must be greater than {ACCESS_KEY_ALLOWANCE}."
);

#[ext_contract(ext_self)]
pub trait ExtLinkDrop {
    /// Callback after creating account and claiming linkdrop.
    fn on_account_created_and_claimed(
      &mut self, 
      amount: U128
    ) -> bool;
}


#[near_bindgen]
impl LinkDrop {
    /// Allows given public key to claim sent balance. 
    /// Takes ACCESS_KEY_ALLOWANCE as fee from deposit to cover
    /// account creation via an access key. 
    #[payable]
    pub fn send(
      &mut self, 
      public_key: PublicKey
    ) -> Promise {
      require!(
        env::attached_deposit() > ACCESS_KEY_ALLOWANCE,
        ERR_MSG_SEND,  // FIRST CHANGE
      );

      let value = self.accounts.get(&public_key).unwrap_or(0);

      self.accounts.insert(
        &public_key,
        // SECOND CHANGE
        &(value + env::attached_deposit()),
        // &(value + env::attached_deposit() - ACCESS_KEY_ALLOWANCE),
      );

      Promise::new(env::current_account_id()).add_access_key(
        public_key, 
        ACCESS_KEY_ALLOWANCE, 
        env::current_account_id(), 
        "claim,create_account_and_claim".to_owned(),
      )
    }

    /// Returns the balance associated with given key.
    pub fn get_key_balance(
      &self, 
      key: PublicKey
    ) -> U128 {
      expect_lightweight(
        self.accounts.get(&key.into()),
        "Key is missing"
      ).into()
    }
}


// =============================== TESTS ======================== //

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;
    use near_sdk::test_utils::VMContextBuilder;
    use near_sdk::{testing_env, VMContext, PublicKey};
    use std::convert::TryInto;

    fn linkdrop() -> AccountId {
      "linkdrop.near".parse().unwrap()
    }

    fn bob() -> AccountId {
      "bob.near".parse().unwrap()
    }

    fn publickey_1() -> PublicKey {
      "ed25519:qSq3LoufLvTCTNGC3LJePMDGrok8dHMQ5A1YD9psbiz"
      .parse()
      .unwrap()
    }


    #[test]
    fn test_get_balance_success() {
      let mut contract = LinkDrop::default();
      let pk: PublicKey = publickey_1();
      let deposit = ACCESS_KEY_ALLOWANCE * 100;

      testing_env!(VMContextBuilder::new()
        .current_account_id(linkdrop())
        .attached_deposit(deposit)
        .build()
      );

      // Send
      contract.send(pk.clone());

      // get balance and assert eq. 
      let balance: u128 = contract.get_key_balance(
        pk
      ).try_into().unwrap();

      assert_eq!(
        balance,
        deposit
      );
    }

}

Ignoring all the warnings, we got:

    Finished test [unoptimized + debuginfo] target(s) in 5.77s
     Running unittests (target/debug/deps/linkdrop-0bce3ee0ea5281ec)

running 1 test
test tests::test_get_balance_success ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

As we mentioned before, if it fails and we need to minus by the ACCESS_KEY_ALLOWANCE, we can always change that later, including the tests.

Next, we shall look at claim. This function isn't necessary actually, and we're never using it. It exists solely because unit tests requires it; if we remove the corresponding tests from the unit tests, we can remove this function.

References

How to navigate Cargo

Optional Appendix Chapter

These contains the information studied in NEAR Certified Developer Level 1 program. These will link to the videos and one's summary of some concept from the videos. It's better if you watch the video yourself rather than just relying on my summary because one doesn't make it very exhaustive.

Peer to Peer Money in Historical Context: Bitcoin

Check out the video: https://www.youtube.com/watch?v=n-EpKQ6xIJs

In this video, he speaks of money as a source of communication, a social construct.

Then he explained of how money had evolved. First, we have objects that are useful for us, with barter trades. Banana trades for salt. And you can eat the food that is traded. Later on, feathers, rocks, etc are given intrinsic value that can exchange for items.

But the big changes come when precious metals (gold, silver, etc) are used as money. It takes skepticism to accept such rule, since gold is not edible.

And then there's the paper money, where you have your gold locked in someone's vault, trusting them for it, and got issued paper money. Paper itself doesn't have any value, and it's skepticism to people again to trust such parties to keep their money. It got accepted anyways.

And finally, plastic cards (bank cards). There's no physical "money" anymore, but digital. Another skeptical changes.

And finally cryptocurrencies (Bitcoin). Bitcoin is an acceptance as a network. It is not simply a payment system, unlike bank cards, which contains digital money. It is the decentralization of money, rather than having an authority holding your money. You own the money yourself, no one own it for you.

The bank gives you a small sum of interest per year, but it loans your money to other people requiring loaning in exchange for a large sum of interest. That's how they earn money. And you have no control over your money being loaned to others. Authority also have the right to prevent you from opening a bank account, if you're against the legal rights, or whatever reason.

But with cryptocurrencies, Bitcoin, you don't owe anyone anything. They're yours forever. You control your bitcoin. You cannot control others' Bitcoin. Bitcoin is transnational, borderless.

Conglomerate, people whom wants control, aren't really scared of criminals using Bitcoin. What they're scared of, is losing control of everyone's money. They scared all of us use Bitcoin (cryptocurrencies).

Questions and answers

He speaks of intrinsic utility of cryptocurrency, which is making Bitcoin much more than just money: turning it into an internet of money.

Bitcoin isn't efficient, because it's like democracy vs monarchy. Monarchy is efficient: one person said it's so and so, and everyone follows. This is same as centralization today. Decentralization is like democracy: it isn't the most efficient method, but it's better.

Bitcoin should not be aiming to conquer central banking, nor trying to fight with any existing structure today. Instead, it doesn't need to. Banking system today will go obsolete per se, without Bitcoin existing. They fail on their own.

Infrastructure Inversion

Original video: https://www.youtube.com/watch?v=5ca70mCCf2M

When new infrastructure is laid on top of old infrastructure, it creates conflict. Infrastructure inversion means having a new infrastructure operating, while an old infrastructure move on top of the new infrastructure and operate on it, as a subset.

If you try to squeeze Bitcoin on the current infrastructure, it wouldn't work. "It's not working, it's slow". Of course, because the infrastructure is not right.

For any new disruptive technology, resistance is the first reaction. Only when crazy pioneers persisted, and show the world they're right, then it changes. And things doesn't show up as it should be in the first place. Only when infrastructure are laid out, they make sense.

Example: trying to replace horses with automobiles. Having an automobile on a muddy road, filled with lots of holes and horse poos, just aren't working. However, an asphalt road (new infrastructure) allows a car to ride on top of it. In addition, horses could also walk on top of asphalt road (if they want to, and take care of their poos).

Another is electricity. Without the infrastructure to deliver electricity to every housing, people can't see it working out. The infrastructure have to be there before it could be working.

Modem is another thing. Telephone lines are designed with short narrow ranges fits to human voices, not to transfer data. Trying to transfer data over telephone lines are crazy. However, due to very little users of internet, coaxial cable and fiber optics aren't built yet to transmit the signals. Internet was carried over the phone reluctantly.

Then, some phone providers later evolved to become ISP, upgrade their infrastructure to coaxial cables rather than telephone lines, and now internet thrives, as the infrastructure changes. What more, telephone calls could now run on top of coaxial cables, without the noisy signals that carried over telephone lines. In fact, there's the irony invention of "comfort noise generation" by telephone company: when you cannot hear noise, you don't know if the other side hung up, so this program pings the other side to see if it hungs up or not, by providing the white noise it generates, at the price of not hearing clearly the voices tranmistted over. They cannot accept a noiseless infrastructure.

(P.S. to hear more of the story, watch the video; )

Now, Bitcoin is running on mainframe-based banking. This is not true. Supposingly, an infrastructure inversion: to have Bitcoin as the dominant generic way of running things, while central banking migrate their service, building them on top of Bitcoin. Traditional banking as an application on top of a decentralized trusted ledger. This is trivial.

Question and answers

Don't apply policy based on whether criminal uses it. Look at the bigger picture: does it freeds everyone for financial freedom? If yes, then it's worth moving forward. Whatever drawbacks, certainly there'll be people who come in and solve it, or at least mitigate it to least risk.

We don't need others perception whether it work out or not. Until it work out, perception wouldn't be good. Based on past experience on disruptive inventions (airplanes, electricity, etc), history are rewritten to say how clever they was, but during the time of invention, they're not praised anyways: they're said to be "not going to work".

And Bitcoin needs many people to start using it: adoption. Usefulness will increase when everyone starts using it. Social and economic infrastructure is what needs to be built, not physical infrastructure. It runs on the internet, and the internet (physical infrastructure) is already doing great.

You know infrastructure inversion when people compared things to Bitcoin (or other cryptocurrencies) as a baseline, rather than USD as the baseline (or other money).

Bitcoin is interesting not in the recording of transaction of blocks, but the decentralization. Blockchain doesn't have direct control that conglomerate wants. No need for identity checks, no need for KYC, etc. These are restricting people from joining the system. Decentralization doesn't need those. Anonymity is fine, it's still working anyways.

Bitcoin is not a zero sum game. It doesn't need to dominate the market. Other cryptocurrencies are welcome to join the game.

The biggest challenge for adoption is letting normal people use it. It's clever people called "wallet" a wallet rather than a "keychain", although in fact, the wallet that stores your cryptocurrencies are really keychain. We need to have a language that people can understand.

Hence, how to make it easier to use and secure, for people whom are not developers, just normal people?

Own opinion

Example, staking. If one is a normal person, one wouldn't want to understand what it means by staking to use staking. One just want to stake and earn some money. And one don't want to need to choose which validators to stake to: that's absurd. So, for the design of people who understand these, these are fine. But for normal people, the UX designed should not need these. Clever people should build something that says, ok, these validators are trustworthy, and automatically stake their money for them without the user choosing which validator and do their own research. Someone do the research for them and make sure the user doesn't lose money. Or even, user doesn't feel like they migrate from a current bank system to Bitcoin: it just works like having 2 accounts: a normal and a saving. A normal that can transfer money in and out, and a saving that are staking, or perhaps, flexi saving that you can pull out your money anytime.

Money as a content type

Check the video: https://www.youtube.com/watch?v=3mUcpsbnhGE

Bitcoin introduced a transformation in which money will be viewed independently of the underlying transport medium, turning it into a standalone content type.

For example, transaction of Bitcoin is a signed data structure that can be executed anywhere in the world. The transaction doesn't necessary needs to happen over Bitcoin network, but could be in any form of network through any form of communication medium.

The reason is, security isn't really needed in what is being transferred. If a hacker hacks that transaction, what he get is not the money, just the sender ID, receiver ID, and the transaction amount. To authorize the transaction, the hacker needs the private key, which is kept always in the receivers' computer. No sensitive information is transmitted out to the internet.

This is not similar to what we have today with bank authorization. You need to insert the bank account details, including sensitive information like CVV to authorize the transaction. These sensitive information are encrypted along the way to the destination. However, during transaction, there are points where the encrypted details are left, and these aren't removed to have a history of what transactions happened. This design is crazy, because no security can really protect a centralized structure from hackers: it either is already hacked, or it's on their way of being hacked.

With no need of sensitive information transferred, things become safer with cryptocurrencies. If the transaction is not signed, transaction fails, no money is transferred, they're still safe. What is required is just to initialize the transaction once again, or until it succeeded. As long as a signed receipt reaches the miner to validate it, the transaction will succeed.

And the signed receipt cannot be changed in any ways, as it would compromise the validity of the receipt. Thus, if say the receiver ID is changed, the receipt will be rendered useless, it cannot be validated by the miners, and hence transaction will not occur. This shows how safe it is. The only way to really steal a transaction is holding the private key, which you requires hacker to hack the receiver's computer to get that.

Even so, hackers need to hacks lots of computers to get lots of private keys to steal a considerable sum, assuming these people aren't aggregating a large sum in their account, but averaged out. This can be compared to hacking a central computer where all sensitive data is being stored, once, and all is done.

Bitcoin transaction is also very small, just 250 bytes. It cannot be censored by any parties: because if a party block the Bitcoin Network, they could be transact through any other means, as long as internet is available. Even if internet is not available, it could go through telephone lines for transaction, or even morse code, or whatever means. 250 bytes is very little amount of information. Unless all communication forms are isolated (banned) from the outside world, nothing else could stop such transaction.

What most people misunderstood is, if the medium is almost costless, the content is worthless. That's not true. Medium doesn't need to be grandiose.

Conglomerate thought controlling the medium was the source of quality. They thought, once quality disappears, they can only clung to more control, which is thought to provides more quality. Gatekeepers thought expensive medium is the only way for the message to be worth listening to. That's not true.

The widening access, opening up of availability, and broadening the range of expression of the medium is thought to lead to trivial, vulgar, and cheapening of the message. Not true. Perhaps there will be at first, but later it turn out not when a larger collective starts using it.

Gatekeepers currently constraint the medium to have a high level of entry. You cannot send a small sum (20 cents) to people via traditional banks, transnationally. It isn't enough to pay for "the value of their service". They cling to the medium and fail to see the message can now be transported over any medium at zero cost, instantaneously.

(Note: of course, gas fee congestion is another story. If you solves gas fee congestion, then this should be possible, although nowadays you still need to be sufficiently large amount to pay gas fee for transaction to authorize; and on NEAR, gas fee is about 0.02 USD).

(Here talks more about other stuffs, which requires you to watch the video to gather more information).

Conclusion: we have now separated the message from the medium. Money is now the content type, and we'll never go back.

Other Notable Resources

There are other resources listed here that are notable. In general, these resources are very long and a topic of their own, either in podcasts or videos. Hence, they aren't summarized here, requiring you to access that particular resources manually.

We will give a brief introduction in one paragraph for the resources available, though, just to let you decide whether you want to read about it or not.

Token Economics

Link here

Not really about NEAR, but explains how the general token economics for blockchain is. The discussion is more about why we are creating these cryptocurrencies other than for virtual digital money. It also explains in what aspect blockchain differs from existing centralized architecture, what improvement it made on top of what's already available. Watching the video convinces yourself that what you build matters, and how it matters, why it matters.

Check out [How one thought the future is good][future] for one's thought and agreement on this.

Altcoins: The history of Failure

Link here

This covers on the various altcoins and how they fail, and what point of failure they have, what their risks are, compared to Bitcoin. This includes the drawbacks of Ethereum, Dogecoin, Tether, Monero, and a lot of other coins on how they solve some issues, while how they're not perfect.

The History of Bitcoin

Link here

Well, this is as the headline suggests, history of Bitcoin. It also talks a little about other coins that diverges from Bitcoin, and why they fail and Bitcoin succeeded in the market. Reasonings on what coins to contribute to (i.e. staying with Bitcoin) is also present.

History before Bitcoin

Link here

This talks about the stories before Bitcoin was invented, how people's idea link together towards Bitcoin invention, and from these inventions and ideas, Satoshi managed to put them together in a really clever way and created Bitcoin. It shows progression towards Bitcoin. Reading it is like reading (fictional) storybook, or if you love, history stories; very enticing. If you have the time, have a go.

Shelling out

Link here

If you're a fan of history, and want to look at how money evolved in the past (actually, how it is necessary to create money in the first place, then only how it changes), this is the go-to articles. It's an article, but a really long one; one would say it a "short story" instead.

Abstract taken from the page: The precursors of money, along with language, enabled early modern humans to solve problems of cooperation that other animals cannot – including problems of reciprocal altruism, kin altruism, and the mitigation of aggression. These precursors shared with non-fiat currencies very specific characteristics – they were not merely symbolic or decorative objects.

Measuring Value

Link here

#TODO

The Right to Read: A Dystopian Short Story

Link here

This shows what might happen in the future, and what has already happening now, illustrated with a short story. Afterwards, some of the modern acts are listed in this article as well. For example: how "trusted computing", from the writer's point of view, isn't really "trustible" as anything saved in your computer can be accessed by the providers and whoever having the power to access them. This is upgraded with TPM (the requirement to upgrade to Windows 11). (Of course, you might want to read from both sides for a more neutral viewpoint and decide for yourself whether it is good or bad, and don't totally believe what one wrote if any as that might include one's opinion rather than factual information ).

Also, check this link out on further information.

Bitcoin is Time

Link here

An article speaking about how Satoshi rethink time allows Bitcoin to come into existence.

How one thought the future is good

What one likes most about is "we can identify what people are contributing to an enterprise". It doesn't necessary be a conglomerate, but assume you're in a project, you can calculate their contribution not just via github pushes for example, but reviewed code to signify how strong is their contribution, whether their contribution make a difference (its importance), etc etc that gives value to people.

It gives you not only the freedom to choose what you want to work with, but not having to comply to a hierarchical manager micromanaging you, etc. If you're in a good workplace, fine. If you're in a bad workplace, one almost certain you will benefit (unless you love being beaten up) from this change.

And in the future, the most important thing is capital, and by capital one don't just mean knowledge capital, but other capital as well, though they're more abstract. Let's see some of them:

Capital

Knowledge capital is the major here, allowing you to contribute to the work. By learning lots of things, you allow yourself to contribute in lots of aspect. You're a working game designer but are bored of it and want to move to machine learning builder (i.e. people call ML engineer, but I don't like the term "engineer", for personal reasons), but you don't have previous experience to get yourself hired? Don't worry, learn the stuff and contribute right away. Don't hassle with trying to pass the interview or whatever. Someone will review whether your code matters and how much it matters.

On the contrary, if you're someone who aced the interviews, whom pass tests, but really lazy at contributing, or decide you would only contribute in the minimal required fashion, you're in big trouble in this world. Survival of the fittest. If you cannot make yourself "fit enough", you can't survive in this field. One suggests you change yourself or your attitude towards how to work now, for smoother transfer when the time came.

And on other capital, one means social capital, etc. Since token exchange could be between friends (probably with real value, perhaps just tokens that are like souvenirs you buy for your friends, which you buy so it has some monetary value), social capital might be important as well.

And many more one won't discuss here.

It's a beautiful world ahead of us. We won't know what really lies ahead of us; overall, we're just following the path of evolution, for the better or for the worse. We will know when it's finally in place, and continue our road of evolution towards the better.

Thank you for reading.