To reiterate, we'd like anyone to be able to participate in the crossword puzzle, even folks who don't have a NEAR account.
The first person to win will "reserve their spot" and choose where to send the prize money: either an account they own or an account they'd like to create.
When a user first visits the crossword, they only see the crossword. No login button and no fields (like a
memo field) to fill out.
On their first visit, our frontend will create a brand new, random seed phrase in their browser. We'll use this seed phrase to create the user's unique key pair. If a random seed phrase is already there, it skips this part. (We covered the code for this in a previous section.)
If the user is the first to solve the puzzle, it discovers the function-call access key and calls
submit_solution with that key. It's basically using someone else's key, as this key is on the crossword account.
We'll be adding a new parameter to the
submit_solution so the user can include the random, public key we just stored in their browser.
During the execution of
submit_solution, because contracts can use Promises to perform Actions, we'll remove the solution public key and add the user's public key.
This will lock out other attempts to solve the crossword puzzle and ensure there is only one winner.
This means that a puzzle can have three states it can be in:
- Solved and not yet claimed (not paid out)
- Claimed and finalized
The previous chapter we discussed enums, so this is simply modifying the enumeration variants.
First, let's see how the
submit_solution will verify the correct solution.
Instead of hashing the plaintext, we simply check that the public key matches what we know the answer is. (The answer being the series of words representing the solution to the crossword puzzle, used as a seed phrase to create a key pair, including a public key.)
Further down in the
submit_solution method we'll follow our plan by adding a function-call access key (that only the winner has) and removing the access key that was discovered by the winner, so no one else can use it.
The first promise above adds an access key, and the second deletes the access key on the account that was derived from the solution as a seed phrase.
Note that the new function-call access key is able to call two methods we'll be adding:
claim_reward— when the user has an existing account and wishes to send the prize to it
claim_reward_new_account— when the user doesn't have an account, wants to create one and send the prize to it
Both functions will do cross-contract calls and use callbacks to see the result. We finally get to the meat of this chapter, let's go!
We're going to be making a cross-contract call to the linkdrop account deployed to the
testnet account. We're also going to have callbacks for that, and for a simple transfer to a (potentially existing) account. For both of these, we'll want to add our special traits like we saw on the linkdrop contract before.
So we'll use
ext_linkdrop for making the cross-contract call and
ext_self to call "ourselves" at a callback.
Again, this function is called when the user solves the crossword puzzle and wishes to send the prize money to an existing account.
Seems straightforward, so why would we need a callback? We didn't use a callback in the previous chapter when the user logged in, so what gives?
It's possible that while claiming the prize, the user accidentally fat-fingers their username, or their cat jumps on their keyboard. Instead of typing
mike.testnet they type
mike.testnzzz and hit send. In short, if we try to send the prize to a non-existent account, we want to catch that.
For brevity, we'll skip some code in this function to focus on the Promise and callback:
Your IDE is your friend
Oftentimes, the IDE can help you.
For instance, in the above snippet we have
receiver_acc_id.parse().unwrap() which might look confusing. You can lean on code examples or documentation to see how this is done, or you can utilize the suggestions from your IDE.
claim_reward method will attempt to use the
Transfer Action to send NEAR to the account specified. It might fail on a protocol level (as opposed to a smart contract failure), which would indicate the account doesn't exist.
Let's see how we check this in the callback:
Notice that above the function, we have declared it to be private.
This is an ergonomic helper that checks to make sure the predecessor is the current account ID.
We actually saw this done "the long way" in the callback for the linkdrop contract in the previous section.
Every callback will want to have this
#[private] macro above it.
The snippet above essentially says it expects there to be a Promise result for exactly one Promise, and then sees if that was successful or not. Note that we're not actually getting a value in this callback, just if it succeeded or failed.
If it succeeded, we proceed to finalize the puzzle, like setting its status to be claimed and finished, removing it from the
unsolved_puzzles collection, etc.
Now we want to handle a more interesting case. We're going to do a cross-contract call to the smart contract located on
testnet and ask it to create an account for us. This name might be unavailable, and this time we get to write a callback that gets a value.
Again, for brevity, we'll show the meat of the
Then the callback:
In the above snippet, there's one difference from the callback we saw in
claim_reward: we capture the value returned from the smart contract we just called. Since the linkdrop contract returns a bool, we can expect that type. (See the comments with "NOTE:" above, highlighting this.)
The previous callback for the
callback_after_create_account has comments around the parameters. ("First parameter", "second parameter", etc.)
It might feel odd at first to do cross-contract calls because of the three "magic" parameters.
This is how the callback parameters need to be for a callback that takes two parameters:
Consider changing contract state in callback
It's not always the case, but often you'll want to change the contract state in the callback.
The callback is a safe place where we have knowledge of what's happened after cross-contract calls or Actions. If your smart contract is changing state before doing a cross-contract call, make sure there's a good reason for it. It might be best to move this logic into the callback.
So what parameters should I pass into a callback?
There's no one-size-fits-all solution, but perhaps there's some advice that can be helpful.
Try to pass parameters that would be unwise to trust coming from another source. For instance, if an account calls a method to transfer some digital asset, and you need to do a cross-contract call, don't rely on the results of contract call to determine ownership. If the original function call determines the owner of a digital asset, pass this to the callback.
Passing parameters to callbacks is also a handy way to save fetching data from persistent collections twice: once in the initial method and again in the callback. Instead, just pass them along and save some CPU cycles.
The last simple change in this section is to modify the way we verify if a user has found the crossword solution.
In previous chapters we hashed the plaintext solution and compared it to the known solution's hash.
Here we're able to simply check the signer's public key, which is available in the
env object under
We'll do this check in both when the solution is submitted, and when the prize is claimed.