Skip to main content

Contract

Feel free to explore the contract code in full. In this tutorial, we assume a basic understanding of Rust and NEAR contracts as we'll only look at the parts relevant to chain signatures. The main data the contract stores is a map of the subscribers and when they last paid the subscription fee.


Constructing the transaction​

We only want the smart contract to be able to sign transactions to charge the subscriber 5 NEAR tokens, no other transactions should be allowed to be signed. This is why we construct the transaction inside of the contract with the omni-transaction-rs library.

NEAR transactions have different Actions, a list of these actions can be found in this section of the docs. It would be natural to assume that the Transfer action would be most appropriate here since we are just sending tokens from one account to another, but in fact we'll use the FunctionCall action since it will allow us to verify that the transaction has actually been sent and excepted by the network.

Why a function call instead of a transfer?

There are two reasons for this:

  1. Just because a transaction has been signed it does not mean it has been sent to the network. We don't want to update the state of the contract to say that the subscription has been paid just because the transaction has been signed, we need to confirm that the transaction has been sent and accepted by the network, the best way to do this in a contract is to call a function on the contract.

  2. The MPC contract can sign transactions that are deemed to be "invalid". The MPC could sign a transaction to send 5 $NEAR from the subscriber to the contract but the subscriber might not actually have 5 $NEAR in their account. As a result, the transaction would be invalid and the network would reject it. With similar reasoning to the first point, we need to confirm that the transaction has been accepted by the network before we update the state of the contract.

Once we have the action we can build the transaction as a whole. You can see that the transaction requires the public_key of the sender, the nonce of the key, and a recent block_hash, we take all of these as arguments to the function since the nonce and the block hash are not accessible inside of the context of the contract, and the public key is much easier to derive outside of the contract, plus we'll save ourselves some gas by doing it this way.

After we make the call to the MPC contract to sign the transaction we'll need to original transaction to create a fully signed transaction therefore we are going to pass the transaction to the callback function. Before the do that we need to serialize the transaction to a JSON string since many of the types used in the transaction are not serializable by default.

The MPC contract takes a transaction payload as an argument instead of the transaction directly. To get the transaction payload we serialize the transaction to borsh using .build_for_signing(), then produce a SHA256 hash of the result and finally convert it to a 32-byte array.


Calling the MPC contract​

In our signer.rs file, we have defined the interface for the sign method on the MPC contract.

As an input, it takes the payload, the path and the key_version. The path determines which public key the MPC contract should use to sign the transaction, in this example the path is the account Id of the subscriber, so each subscriber has a unique key. The key_version states which key type is being used. Currently the only key type supported is secp256k1 which has a key version of 0.

We then make a cross contract call to the sign function on the MPC contract and make a callback with the JSON stringified transaction.

We attach a small amount of gas to the callback and use a gas weight of 0 so the majority of the attached gas can be used by the MPC contract.


Reconstructing the signature​

Once the transaction has been signed by the MPC contract we can reconstruct the signature and add it to the transaction. You could decide to reconstruct the signature and add it to the transaction outside of the contract, but an advantage of doing it in the contract is that you can return a fully signed transaction from the contract which can be straight away sent to the network instead of having to store the transaction in the frontend. It also makes it much easier for indexers/relayers to get transactions and broadcast them, making it less likely that transactions will be signed without being sent.

The MPC contract returns the signature in a structure called SignResult that contains the three portions of the signature: big_r, s and the recovery_id often referred to as v. Note that r and s are wrapped in the additional structures AffinePoint and Scalar respectively.

We receive the parts of the signature as hex strings. We need to convert them to bytes, remember that two hex characters make a single byte. A NEAR secp256k1 signature is 65 bytes long, the first 32 bytes being r (where r is the first 32 bytes of big_r, which is 34 bytes long itself), the next 32 bytes are all the bytes from s and the final byte is the last byte of big_r. The recovery id is not used in this case. We then use a method to convert the bytes to a secp256k1 signature

The final step is to deserialize the transaction we passed and add the signature to it. Now we can return the fully signed transaction.


Receiving payment​

Once the signed transaction is relayed to the NEAR network it will call the pay_subscription method in the contract. You can see that we are only updating the state of the contract here when the transaction has been accepted by the network.


In the next section we'll look at the key parts of the scripts that interact with the contract which will show how to derive the subscriber's MPC public key and get the data outside the contract required to build the transaction inside.

Was this page helpful?