-
Notifications
You must be signed in to change notification settings - Fork 377
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
--- | ||
id: near-example | ||
title: Controlling a NEAR account with a contract | ||
--- | ||
|
||
import {Github, Language} from "@site/src/components/codetabs" | ||
|
||
This example is of a `simple subscription service` that allows a user to subscribe to an arbitary service and allows the contract to charge them 5 NEAR tokens every month. For most chains an account as a single key, the power of NEARs account model combined with chain signatures is that you can add an `MPC controlled key` to your account and allow a contract to control your account through code and limited actions (including ones that require a full access key to sign). You can also dervie and use new implicit NEAR accounts via the MPC contract as you do with other chains but this usecase is cooler. | ||
Check failure on line 8 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/near-example.md GitHub Actions / runner / misspell
|
||
|
||
This concept also enables: | ||
- **Account recovery**: allow a contract to add a new private key to your account after preset conditions are met. | ||
- **Trail accounts**: the [Keypom](https://github.com/keypom) contract uses this concept to create trial accounts that can only peform a limited number of actions (including those that require a full access key) and can be upgraded to a full account upon the completion of specified actions. These accounts are also multichain. | ||
- **DCA service**: a contract that allows a DEX to buy a token for a user every fixed period with a pre defined amount of USDC. | ||
- **and more...** | ||
|
||
These were all previously possible - before chain signatures - since a NEAR account is also a smart contract, but this required the user to consume a lot of $NEAR in storage costs to upload the contract to the account and it lacked flexability. This approach is much more scalable and new account services can be switched in and out easily. | ||
|
||
Since a NEAR account is also a multichain account, any dervied foreign accounts associated with the NEAR account also inherit these account services. | ||
|
||
--- | ||
|
||
# Running the example | ||
|
||
This example has contracts written in rust and scripts to interact with the contract in NodeJS. | ||
|
||
Go ahead and clone the repository to get started: | ||
|
||
```bash | ||
# Clone the repository | ||
git clone https://github.com/PiVortex/subscription-example.git | ||
|
||
# Navigate to the scripts directory | ||
cd subscription-example/scripts | ||
|
||
# Install the dependencies | ||
npm install | ||
``` | ||
|
||
To interact with the contract you will need three different accounts. A subscriber, an admin and a contract. Run the following command to create the accounts and deploy the contract: | ||
|
||
```bash | ||
npm run setup | ||
``` | ||
|
||
To subscribe to the service run the following command: | ||
|
||
```bash | ||
npm run subscribe | ||
``` | ||
|
||
To charge the subscriber from the admin account run the command: | ||
|
||
```bash | ||
npm run charge | ||
``` | ||
|
||
To unsubscribe from the service run the command: | ||
|
||
```bash | ||
npm run unsubscribe | ||
``` | ||
|
||
--- | ||
|
||
# Contract overview | ||
|
||
Feel free to explore the contract code in full. In this tutorial we assume a basic understanding of Rust and NEAR contracts and will only look at the relevant 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](../../../../1.concepts/protocol/transaction-anatomy.md#actions). One might think that the `Transfer` action would be most applicable here since we are just sending tokens from one account to another, but in facts we will use the `FunctionCall` action since it will alllow us to verify that the transaction has actually been sent and excepted by the network. | ||
|
||
<details> | ||
<summary> Why a function call instead of a transfer? </summary> | ||
|
||
There are two reasons for this: | ||
1) Just because a transaction has been signed it does not mean it has been sent. 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 deamed 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. | ||
|
||
</details> | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L45-L54" | ||
start="45" end="54" /> | ||
</Language> | ||
|
||
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 blockhash are not accesible 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 outside of the contract. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L56-L69" | ||
start="56" end="69" /> | ||
</Language> | ||
|
||
After we make the call to the MPC contract to sign the transaction we will 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. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L71-L74" | ||
start="71" end="74" /> | ||
</Language> | ||
|
||
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 we proudce a SHA256 hash of the result and finally convert it to 32-byte array. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L76-L81" | ||
start="76" end="81" /> | ||
</Language> | ||
|
||
## Calling the MPC contract | ||
|
||
In our `signer.rs` file we have defined the interface for the `sign` method on the MPC contract. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="signer.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/signer.rs#L40-L43" | ||
start="40" end="43" /> | ||
</Language> | ||
|
||
As 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 identifiable key. The full path is a combination of the `predeccessor` to the MPC contract (the subscription contract) along with the path given as the argument. The addition of the predeccessor means that only this contract is able to sign transactions for the given 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`. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="signer.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/signer.rs#L3-L18" | ||
start="3" end="18" /> | ||
</Language> | ||
|
||
We then make a `cross contract call` to the MPC contract on the method `sign` and make a callback with the JSON stringified transaction. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L87-L98" | ||
start="87" end="98" /> | ||
</Language> | ||
|
||
We attach a small amount of gas to the callback and use a `gas weight of 0` so the majority of the attatched 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 in the client side, 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 braodcasted to the network instead of having to store the transaction in the frontend. This 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 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. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="signer.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/signer.rs#L20-L38" | ||
start="20" end="38" /> | ||
</Language> | ||
|
||
We recieve the parts of the singature 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 an secp256k1 signature | ||
Check failure on line 159 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/near-example.md GitHub Actions / runner / misspell
Check failure on line 159 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/near-example.md GitHub Actions / runner / misspell
|
||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L109-L129" | ||
start="109" end="129" /> | ||
</Language> | ||
|
||
The final step is to `deserlialize` the transaction we passed and add the signature to it. Now we can return the `fully signed transaction`. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="charge_subscription.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/charge_subscription.rs#L110-L130" | ||
start="131" end="139" /> | ||
</Language> | ||
|
||
## Recieve payment method | ||
Check failure on line 175 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/near-example.md GitHub Actions / runner / misspell
|
||
|
||
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. | ||
|
||
<Language language="rust" showSingleFName={true}> | ||
<Github fname="lib.rs" | ||
url="https://github.com/PiVortex/subscription-example/blob/main/contract/src/lib.rs#L61-L79" | ||
start="61" end="79" /> | ||
</Language> | ||
|
||
# Scripts overview | ||
|
||
We implement a few scripts to interact with the contract. This is not just as simple as calling a method in the contract as we will have to do manage the MPC public key added to the subscriber account. | ||
|
||
## Subscribe | ||
|
||
## Charge | ||
|
||
## Unsubscribe |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
--- | ||
id: overview | ||
title: Chain signature in a NEAR contract | ||
--- | ||
|
||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
import {CodeTabs, Language, Github} from "@site/src/components/codetabs" | ||
|
||
A key part of the chain signatures technology is the ability for a smart contract to be able to call the MPC contract to sign transactions on other chains. This enables you to implement smart contract logic on top of other chains and inherit NEAR's account model, finality and gas price. This becomes an especially powerful tool when this is used for non smart contract chains like Bitcoin as now they essentially have a smart contract layer on top of them. | ||
|
||
A library that helps us implement this is [omni-transaction-rs](https://github.com/near/omni-transaction-rs); it allows us build transactions inside of a smart contract to then be signed by the MPC contract. Building a transaction inside of the contract instead of the client allows for only certain transactions to be signed by the contract. | ||
|
||
--- | ||
|
||
# Passing relevant information to the contract | ||
|
||
Transactions need some information that can only be fetched off chain such as the nonce and other information that is easiest to fetch off chain like the addresses. This information can be passed to the contract as arguments. Each chain creates transactions differently so the information that needs to be passed to the contract will be different for each chain. | ||
|
||
--- | ||
|
||
# Constructing a transaction | ||
|
||
The library implements a transaction builder for each chain. | ||
|
||
<Tabs groupId="code-tabs"> | ||
<TabItem value="Ξ Ethereum"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="₿ Bitcoin"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="Ⓝ NEAR"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
</Tabs> | ||
|
||
|
||
--- | ||
|
||
# Building the payload | ||
|
||
Once the transaction is built you next need to construct the payload that will be sent to the MPC contract. | ||
|
||
<Tabs groupId="code-tabs"> | ||
<TabItem value="Ξ Ethereum"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="₿ Bitcoin"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="Ⓝ NEAR"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
</Tabs> | ||
|
||
--- | ||
|
||
|
||
# Calling the MPC contract | ||
|
||
Now we have the payload we can request it to be signed by the MPC contract using the `sign` method. | ||
|
||
<Tabs groupId="code-tabs"> | ||
<TabItem value="Ξ Ethereum"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="₿ Bitcoin"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="Ⓝ NEAR"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
|
||
</Tabs> | ||
|
||
|
||
--- | ||
|
||
# Reconstructing the signature | ||
|
||
Note that this step is optional. You can decide to reconstruct the signature and add it to the transaction in the contract or the client, 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 braodcasted to the network instead of having to store the transaction in the frontend. This also makes it much easier for indexers/relayers to get transactions and broadcast them. | ||
|
||
|
||
## Passing the transaction to the callback | ||
|
||
We will reconstruct the signature in the callback. We need to pass the transaction to the callback. First the transaction needs to be serialized as a json string many of the types used in the transaction are not serializable by default. | ||
|
||
<Github language="rust" url=""start="" end="" /> | ||
|
||
We will pass the json string to the callback. | ||
|
||
<Github language="rust" url=""start="" end="" /> | ||
|
||
Then the callback will recieve the result from the mpc contract and the transaction and check that the result is ok. | ||
Check failure on line 95 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/overiew.md GitHub Actions / runner / misspell
|
||
|
||
<Github language="rust" url=""start="" end="" /> | ||
|
||
Now the tranasction is converted back into is respective transaction structure the individual parts of the singature are reconstructed into a single signature and the singature is added to the transaction and returned from the contract. | ||
Check failure on line 99 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/overiew.md GitHub Actions / runner / misspell
Check failure on line 99 in docs/2.build/1.chain-abstraction/chain-signatures/chain-signatures-contract/overiew.md GitHub Actions / runner / misspell
|
||
|
||
<Tabs groupId="code-tabs"> | ||
<TabItem value="Ξ Ethereum"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="₿ Bitcoin"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
<TabItem value="Ⓝ NEAR"> | ||
<Github language="rust" url="" start="" end="" /> | ||
</TabItem> | ||
|
||
</Tabs> | ||
|
||
|
||
--- | ||
|
||
# Relaying the signed transaction | ||
|
||
|
||
|
||
--- | ||
|
||
# Cookbook | ||
:::info Cookbook | ||
|
||
Further examples of building different transactions can be found in the [cookbook](https://github.com/Omni-rs/examples/tree/main). | ||
|
||
::: |