Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved relayer learn and build pages #1820

Merged
merged 11 commits into from
Mar 25, 2024
13 changes: 7 additions & 6 deletions docs/1.concepts/abstraction/relayers.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ id: relayers
title: Relayers
---

A relayer is a simple web service that receives transactions from NEAR users, and relays them to the network while attaching tokens to cover their GAS expenses. In this document we present a high-level overview on how relayers work. Please check the [build a relayer](../../2.develop/relayers/welcome.md) page if you want to learn how to build your own relayer.
A relayer is a simple web service that receives signed transactions from NEAR users, and relays them to the network while attaching tokens to sponsor their GAS expenses. This can be useful to create applications in which the users are not required to purchase NEAR in order to be able to transact. In this document we present a high-level overview on how relayers work. Please check the [build a relayer](../../2.develop/relayers/welcome.md) page if you want to learn how to build your own relayer.

---

## How it works

Relayers are a natural consequence of [Meta Transactions](meta-tx.md) ([NEP-366](https://github.com/near/NEPs/blob/master/neps/nep-0366.md)), a special type of transaction which can be best understood as an intent.
![relayer-overview](/docs/assets/welcome-pages/relayer-overview.png)

The user expresses: _"I want to do a specific action on chain"_ and signs this intent **off-chain**, but does not sends it to the network. Instead, they send the intent to a `Relayer`, which wraps the message into an actual transaction, attaches the necessary funds, and sends it to the network.
Relayers are a natural consequence of [Meta Transactions](meta-tx.md) ([NEP-366](https://github.com/near/NEPs/blob/master/neps/nep-0366.md)), a special type of transaction that can be best understood as an intent.

The user expresses: _"I want to do a specific action on chain"_ and signs this intent **off-chain**, but does not send it to the network. Instead, they send the intent to a `Relayer`, which wraps the message into an actual transaction, attaches the necessary funds, and sends it to the network.

<details>
<summary> Technical Details </summary>
Expand All @@ -25,12 +27,11 @@ The `SignedTransaction` is then sent to the network via RPC call, and the result

---

## Why using a Relayer?

## Why use a Relayer?
There are multiple reasons to use a relayer:
1. Your users are new to NEAR and don't have any gas to cover transactions
2. Your users have an account on NEAR, but only have a Fungible Token Balance. They can now use the FT to pay for gas
3. As an enterprise or a large startup you want to seamlessly onboard your existing users onto NEAR without needing them to worry about gas costs and seed phrases
4. As an enterprise or large startup you have a user base that can generate large spikes of user activity that would congest the network. In this case, the relayer acts as a queue for low urgency transactions
5. In exchange for covering the gas fee costs, relayer operators can limit where users spend their assets while allowing users to have custody and ownership of their assets
6. Capital Efficiency: Without relayer if your business has 1M users they would have to be allocated 0.25 NEAR to cover their gas costs totalling 250k NEAR. However, only ~10% of the users would actually use the full allowance and a large amount of the 250k NEAR is just sitting there unused. So using the relayer, you can allocate 50k NEAR as a global pool of capital for your users, which can refilled on an as needed basis3.
6. Capital Efficiency: Without relayer if your business has 1M users they would have to be allocated 0.25 NEAR to cover their gas costs totalling 250k NEAR. However, only ~10% of the users would actually use the full allowance and a large amount of the 250k NEAR is just sitting there unused. So using the relayer, you can allocate 50k NEAR as a global pool of capital for your users, which can refilled on an as needed basis.
217 changes: 105 additions & 112 deletions docs/2.develop/relayers/welcome.md
Original file line number Diff line number Diff line change
@@ -1,147 +1,140 @@
---
id: build-relayer
title: Building a Meta Transaction Relayer with NextJS
title: Building a Meta Transaction Relayer
sidebar_label: Building a Relayer
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import {CodeTabs, Language, Github} from "@site/src/components/codetabs"

This guide will walk you through the steps required to construct a meta transaction relayer using NextJS 13.
Relayers serve to delegate gas fees to a web service, allowing users to transact on NEAR without the need to acquire the token themselves while still retaining the security of signing their own transactions. This guide will lead you through the components necessary to construct a relayer capable of handling meta transactions.

:::tip
This guide was originally created by [@microchipgnu](https://x.com/microchipgnu) and can be found in [hackmd](https://hackmd.io/@microchipgnu/meta-transactions-relayer-next-js?utm_source=preview-mode&utm_medium=rec).

If you're already acquainted with the technology, you can fast track to a [working open source example](https://github.com/SurgeCode/near-relay-example)

:::

---
For other languages you can check out [Python](https://github.com/here-wallet/near-relay), [Rust](https://github.com/near/pagoda-relayer-rs)

## How it works

![relayer-overview-technical](/docs/assets/welcome-pages/relayer-overview-technical.png)

A basic relayer consists of a web server housing a funded NEAR account. This account receives an encoded signed transaction, which can subsequently be decoded into a `SignedDelegate` format and transmitted on-chain.

The client can then generate a `SignedDelegateAction` (a signed message that hasn't yet been sent), encode it, and transmit it to this server, where it will be relayed onto the blockchain.

## Relayer (server)

## Configuring the Environment
<Tabs groupId="code-tabs">
<TabItem value="near-api-js">

Before we begin, it’s crucial to configure our environment correctly. We need to set some environment variables. This is done in the .env file at the root of your project.
Here's a simple express endpoint deserializes the body, instantiates the relayer account and then sends the transaction.

<Github language='typescript' url='https://github.com/SurgeCode/near-relay-example/blob/main/server.ts' start='16' end='27'/>

You can easily get the account object used to send the transactions from its private key using this snippet

<Github language='typescript' url='https://github.com/SurgeCode/near-relay-example/blob/main/util.ts' start='5' end='17'/>


:::info

The code in the example only works from the following versions onwards

```bash
NEXT_PUBLIC_RELAYER_ACCOUNT_ID=xyz.testnet
RELAYER_ACCOUNT_PRIVATE_KEY=xyz
```
"near-api-js": "3.0.4"
"@near-js/transactions": "1.1.2",
"@near-js/accounts": "1.0.4"
```

The `NEXT_PUBLIC_RELAYER_ACCOUNT_ID` is your public relayer account identifier and the `RELAYER_ACCOUNT_PRIVATE_KEY` is your relayer account’s private key.
:::

## API Route Creation

The next step is creating the API route. For this, we need to take the following actions:
</TabItem>
<TabItem value="@near-relay/server">
@near-relay simplifies meta transactions making it easier to get started for a beginner

1. Navigate to the `./src/app/api/ directory`
2. Inside the api directory, create a new folder called relayer
3. Inside the relayer folder, create a new file named `route.ts`
To start, call the relay method inside an endpoint to automatically deserialize the transaction and send it with the account defined in the environment variables.

Now, let’s fill `route.ts` with the following code:
<Github language='typescript' url='https://github.com/SurgeCode/near-relay/blob/main/server/server.ts' start='8' end='12'/>

```js
import { submitTransaction } from "@/utils/near/meta-transactions";
import { SCHEMA } from "@/utils/near/types/schema";
import { SignedDelegate } from "@near-js/transactions";
import { deserialize } from "borsh";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
const body = await req.json();
const { delegated, network } = body;
If you're interested in relaying account creation as well, it's quite straightforward. Simply create another endpoint and directly call the createAccount method with the accountId and publicKey. These parameters are automatically included in the body when using the corresponding client library.
<Github language='typescript' url='https://github.com/SurgeCode/near-relay/blob/main/server/server.ts' start='14' end='18'/>

</TabItem>
</Tabs>

const deserializeDelegate = deserialize(
SCHEMA,
SignedDelegate,
Buffer.from(new Uint8Array(delegated))
);

const result = await submitTransaction({
delegate: deserializeDelegate,
network,
});

return NextResponse.json(
{ result },
{
status: 200,
headers: {
"content-type": "application/json",
},
}
);
## Client

<Tabs groupId="code-tabs">
<TabItem value="near-api-js">
In this method we are creating an arbitrary smart contract call, instantiating an account and using it to sign but not send the transaction. We can then serialize it and send it to the relayer where it will be delegated via the previously created endpoint.
<Github language='typescript' url='https://github.com/SurgeCode/near-relay-example/blob/main/client.ts' start='10' end='30'/>

</TabItem>
<TabItem value="@near-relay/client">
As mentioned in the above note in order to be able to relay on the client side it's necessary to have access to signing transactions directly on the client. Luckily leveraging the near biometric library it's possible to do so in a non custodial way.

By calling this method and passing in the URL for the account creation endpoint (mentioned in the server section) as well as the `accoundId` everything is handled under the hood to successfully create an account.
<Github language='typescript' url='https://github.com/SurgeCode/near-relay/blob/main/example/src/app/page.tsx' start='17' end='23'/>

On the client side, you just need to create an `Action` and pass it into the `relayTransaction` method along with the URL of the relayer endpoint discussed in the server section and the id of the `receiverId`.

<Github language='typescript' url='https://github.com/SurgeCode/near-relay/blob/main/example/src/app/page.tsx' start='25' end='36'/>
</TabItem>
</Tabs>

<details>
<summary> Relaying with wallets </summary>


At the moment, wallet selector standard doesn't support signing transactions without immediately sending them. This functionality is essential for routing transactions to a relayer. Therefore, to smoothly integrate relaying on the client side, it's necessary to be able to sign transactions without relying on wallets.
Progress is being made to make this possible in the future.
</details>


### Gating the relayer

In most production applications it's expected that you want to be able to gate the relayer to only be used in certain cases.
By taking apart the `delegateAction` object inside the `SignedDelegate`on the server this can be done simply.

```typescript
export declare class DelegateAction extends Assignable {
senderId: string;
receiverId: string;
actions: Array<Action>;
nonce: BN;
maxBlockHeight: BN;
publicKey: PublicKey;
}
```

This script is responsible for receiving a delegated transaction, deserializing it, and submitting the transaction using the submitTransaction utility.
You can, for example, gate by some particular user or contract:

---
```typescript
const serializedTx: Buffer = req.body;
const deserializedTx: SignedDelegate = deserialize(SCHEMA.SignedDelegate, Buffer.from(serializedTx)) as SignedDelegate;
const relayerAccount: Account = await getAccount(NETWORK_ID, RELAYER_ID, RELAYER_PRIVATE_KEY);
const delegateAction = deserializedTx?.delegateAction

### Client Code Execution

The client code is divided into two parts. The first part signs a delegated transaction, and the second part submits the transaction through the relayer.

#### Signing a Delegated Transaction

We use the following code snippet to sign a delegated transaction:

```js
import { getKeys } from "@near-js/biometric-ed25519";
import { InMemoryKeyStore } from "@near-js/keystores";
import { actionCreators } from "@near-js/transactions";
import BN from "bn.js";
import { connect } from "./meta-transactions";

export const signDelegatedTransaction = async ({
network,
signer,
privateKey,
transaction,
contractAddress,
}: {
network: string;
signer: string;
privateKey: string;
transaction: {
methodName: string;
args: any;
gas: string | number;
deposit: string | number;
};
contractAddress: string;
}) => {
const keyStore = new InMemoryKeyStore();

// TODO: Connect the user

const signerAccount = await connect(signer, keyStore, network);

const action = actionCreators.functionCall(
transaction.methodName,
JSON.parse(transaction.args),
new BN(transaction.gas),
new BN(transaction.deposit)
);

const delegate = await signerAccount.signedDelegate({
actions: [action],
blockHeightTtl: 600,
receiverId: contractAddress,
if(delegateAction.senderId == 'someUserId' || delegateAction.receiverId == 'someContractId' ){
const receipt = await relayerAccount.signAndSendTransaction({
actions: [actionCreators.signedDelegate(deserializedTx)],
receiverId: deserializedTx.delegateAction.senderId
});
}

return delegate;
};
```

This function receives several parameters, including `network`, `signer`, `privateKey`, `transaction`, and `contractAddress`. It generates a delegate using the `signedDelegate` method, which is later used to submit the transaction.

#### Submitting the Transaction via the Relayer
Other examples could be looking into the actions and seeing if there is deposit or gas and limiting them, gating by particular smart contract methods or even args.

After signing the delegated transaction, we use the following code snippet to submit the transaction through the relayer:
You can decode the args using:

```js
await fetch("/api/internal/near/submit-meta-transaction", {
body: JSON.stringify({
delegated: Array.from(encodeSignedDelegate(delegated)),
network: "testnet",
}),
headers: {},
method: "POST",
});
```

The `fetch` function sends a POST request to the API route we created earlier. The body of this request includes the delegated transaction and the network.
JSON.parse(Buffer.from(args_base64 || "", "base64").toString())
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading