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

Add gasless backend #450

Merged
merged 6 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/gasless/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NODE_URL=wss://rpc.vara-network.io
VOUCHER_ACCOUNT=0x...
39 changes: 39 additions & 0 deletions backend/gasless/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"root": true,
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": ["airbnb-typescript/base", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"semi": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"indent": [
"warn",
2,
{
"SwitchCase": 1,
"flatTernaryExpressions": false,
"offsetTernaryExpressions": true
}
],
"linebreak-style": ["error", "unix"],
"quotes": ["warn", "single", { "avoidEscape": true }],
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/no-explicit-any": "off",
"no-case-declarations": 0,
"eol-last": "error",
"max-len": ["error", { "code": 120, "tabWidth": 2, "ignoreStrings": true, "ignoreComments": true }],
"array-bracket-spacing": ["error", "always"],
"computed-property-spacing": ["error", "never"],
"no-multi-spaces": "error",
"space-before-function-paren": ["error", "never"]
}
}
12 changes: 12 additions & 0 deletions backend/gasless/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.DS_Store
.vscode/
.binpath
.metahash

# Rust
target/

# Npm
node_modules/
.env
dist/
20 changes: 20 additions & 0 deletions backend/gasless/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM node:20-alpine

ARG REACT_APP_SIGNALING_SERVER \
REACT_APP_NODE_ADDRESS \
PATH_TO_META \
WS_ADDRESS \
PROGRAM_ID
ENV REACT_APP_SIGNALING_SERVER=${REACT_APP_SIGNALING_SERVER} \
REACT_APP_NODE_ADDRESS=${REACT_APP_NODE_ADDRESS} \
PATH_TO_META=${PATH_TO_META} \
WS_ADDRESS=${WS_ADDRESS} \
PROGRAM_ID=${PROGRAM_ID}

WORKDIR /apps
COPY ./backend/w3bstreaming /apps
ElessarST marked this conversation as resolved.
Show resolved Hide resolved

RUN yarn install
RUN yarn build

CMD ["yarn", "start"]
70 changes: 70 additions & 0 deletions backend/gasless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<p align="center">
<a href="https://gear-tech.io">
<img src="https://github.com/gear-tech/gear/blob/master/images/logo-grey.png" width="400" alt="GEAR">
</a>
</p>
<p align=center>
<a href="https://github.com/gear-tech/gear-js/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-GPL%203.0-success"></a>
</p>
<hr>

# Gasless

## Description

Example of backend for issuing and revoking vouchers (gasless).

Example contains three functions for issuing and revoking vouchers:

1. `issue(account: HexString, programId: HexString, amount: number, durationInSec: number): Promise<string>`
- Issues a voucher for the given account, programId, amount, and duration.
- Parameters:
- `account`: The account to issue the voucher for.
- `programId`: The programId to issue the voucher for.
- `amount`: The amount to issue the voucher for.
- `durationInSec`: The duration to issue the voucher for in seconds.
- Returns: A Promise that resolves to the voucherId as a string.

2. `prolong(voucherId: HexString, account: string, balance: number, prolongDurationInSec: number): Promise<void>`
- Prolongs the voucher with the given voucherId, account, balance, and prolongDuration.
- Parameters:
- `voucherId`: The voucherId to prolong.
- `account`: The account to prolong the voucher for.
- `balance`: The required balance to top up the voucher.
- `prolongDurationInSec`: The duration to prolong the voucher for in seconds.
- Returns: A Promise that resolves when the operation is complete.

3. `revoke(voucherId: HexString, account: string): Promise<void>`
- Revokes the voucher with the given voucherId and account.
- Parameters:
- `voucherId`: The voucherId to revoke.
- `account`: The account to revoke the voucher for.
- Returns: A Promise that resolves when the operation is complete.

These functions are part of the `GaslessService` class, which interacts with the Gear API to manage vouchers.


## Getting started

### Install packages:

```sh
yarn install
```

### Declare environment variables:

Create `.env` file, `.env.example` will let you know what variables are expected.


### Build the app:

```sh
yarn build
```

### Run the app:

```sh
yarn start
```
31 changes: 31 additions & 0 deletions backend/gasless/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "gasless",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "rm -rf dist && tsc",
"start": "node dist/main.js",
"watch": "ts-node-dev src/main.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@gear-js/api": "^0.36.3",
"@polkadot/api": "^10.11.2",
"@polkadot/types": "10.10.1",
"@polkadot/wasm-crypto": "^7.3.2",
"@polkadot/util": "^7.3.2",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"socket.io": "^4.6.1"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.10.0",
"ts-node-dev": "^2.0.0",
"types": "*",
"typescript": "^5.0.4"
}
}
144 changes: 144 additions & 0 deletions backend/gasless/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { GearApi, HexString, VoucherIssuedData, IUpdateVoucherParams } from '@gear-js/api';
import { waitReady } from '@polkadot/wasm-crypto';
import { hexToU8a, } from '@polkadot/util';

import { Keyring } from '@polkadot/api';

const secondsToBlock = 3;

export class GaslessService {
ElessarST marked this conversation as resolved.
Show resolved Hide resolved
private api: GearApi;

constructor() {
this.api = new GearApi({ providerAddress: process.env.NODE_URL });
}

/**
* Issues a voucher for the given account, programId, amount, and duration.
*
* @param account - The account to issue the voucher for.
* @param programId - The programId to issue the voucher for.
* @param amount - The amount to issue the voucher for.
* @param durationInSec - The duration to issue the voucher for.
* @returns The voucherId.
*/
async issue(account: HexString, programId: HexString, amount: number, durationInSec: number): Promise<string> {
await Promise.all([this.api.isReadyOrError, waitReady()])
const voucherAccount = this.getVoucherAccount();

const durationInBlocks = Math.round(durationInSec / secondsToBlock);

const { extrinsic } = await this.api.voucher.issue(account, amount * 1e12, durationInBlocks, [programId]);

const [voucherId] = await new Promise<[HexString, HexString]>((resolve, reject) => {
extrinsic.signAndSend(voucherAccount, ({
events,
status,
}) => {
if (status.isInBlock) {
const viEvent = events.find(({ event }) => event.method === 'VoucherIssued');
if (viEvent) {
const data = viEvent.event.data as VoucherIssuedData;
resolve([data.voucherId.toHex(), status.asInBlock.toHex()]);
} else {
const efEvent = events.find(({ event }) => event.method === 'ExtrinsicFailed');

reject(efEvent ? this.api.getExtrinsicFailedError(efEvent?.event) : 'VoucherIssued event not found');
}
}
});
});

return voucherId;
}

/**
* Prolongs the voucher with the given voucherId, account, balance, and prolongDurationInSec.
*
* @param voucherId - The voucherId to prolong
* @param account - The account to prolong the voucher for
* @param balance - The required balance to top up the voucher
* @param prolongDurationInSec - The duration to prolong the voucher for
*/
async prolong(voucherId: HexString, account: string, balance: number, prolongDurationInSec: number) {
const voucherBalance = (await this.api.balance.findOut(voucherId)).toBigInt() / BigInt(1e12);
const durationInBlocks = Math.round(prolongDurationInSec / secondsToBlock);
const voucherAccount = this.getVoucherAccount();

const topUp = BigInt(balance) - voucherBalance;

const params: IUpdateVoucherParams = {};

if (prolongDurationInSec) {
params.prolongDuration = durationInBlocks;
}

if (topUp > 0) {
params.balanceTopUp = topUp * BigInt(1e12);
}

const tx = this.api.voucher.update(account, voucherId, params);

const blockHash = await new Promise<HexString>((resolve, reject) => {
tx.signAndSend(voucherAccount, ({
events,
status,
}) => {
if (status.isInBlock) {
const vuEvent = events.find(({ event }) => event.method === 'VoucherUpdated');
if (vuEvent) {
resolve(status.asInBlock.toHex());
} else {
const efEvent = events.find(({ event }) => event.method === 'ExtrinsicFailed');
if (efEvent) {
reject(JSON.stringify(this.api.getExtrinsicFailedError(efEvent?.event)));
} else {
reject(new Error('VoucherUpdated event not found'));
}
}
}
});
});
}

/**
* Revokes the voucher with the given voucherId and account.
*
* @param voucherId - The voucherId to revoke
* @param account - The account to revoke the voucher for
*/
async revoke(voucherId: HexString, account: string) {
const voucherAccount = this.getVoucherAccount();
const tx = this.api.voucher.revoke(account, voucherId);
await new Promise<HexString>((resolve, reject) => {
tx.signAndSend(voucherAccount, ({
events,
status,
}) => {
if (status.isInBlock) {
const vuEvent = events.find(({ event }) => event.method === 'VoucherRevoked');
if (vuEvent) {
resolve(status.asInBlock.toHex());
} else {
const efEvent = events.find(({ event }) => event.method === 'ExtrinsicFailed');
if (efEvent) {
reject(JSON.stringify(this.api.getExtrinsicFailedError(efEvent?.event)));
} else {
reject(new Error('VoucherRevoked event not found'));
}
}
}
});
});
}

private getVoucherAccount() {
const seed = process.env.VOUCHER_ACCOUNT;
const keyring = new Keyring({
type: 'sr25519',
ss58Format: 137,
});
const voucherAccount = keyring.addFromSeed(hexToU8a(seed));
return voucherAccount;
}
}
Loading
Loading