Skip to content

Commit

Permalink
feat: Account bootstrapping [DEV-3051] (#306)
Browse files Browse the repository at this point in the history
* - Add tokens auto-populating for newcomers
- Add address into account creating response

* - Move to asking scope directly from logto
- refactorings and cleanups

* Add account bootstrapping actions

* Move to use utils from sdk
  • Loading branch information
Andrew Nikitin authored Aug 10, 2023
1 parent 9987900 commit d157535
Show file tree
Hide file tree
Showing 16 changed files with 233 additions and 36 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ By default, `ENABLE_AUTHENTICATION` is set to off/`false`. To enable external Ve
5. **Miscellaneous**
1. `COOKIE_SECRET`: Secret for cookie encryption.

#### Faucet settings

This section describes bootstrapping things for newcomers accounts. If it's enabled the CredentialService auto-populates some tokens on the testnet for making the process simpler.

1. `FAUCET_ENABLED` - enable/disable such functionality (`false` by default)
2. `FAUCET_URI` - URI when the faucet service is located (`https://faucet-api.cheqd.network/credit` by default)
3. `FAUCET_DENOM` - the denom of token to assign (`ncheq` by default)
4. `TESTNET_MINIMUM_BALANCE` - the minimum amount of tokens for being on testnet account. Be default it's amount, required for creating a DID

### 3rd Party Connectors

The app supports 3rd party connectors for credential storage and delivery.
Expand Down
10 changes: 10 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ ARG VERIDA_NETWORK=testnet
ARG POLYGON_RPC_URL=https://rpc-mumbai.maticvigil.com
ARG VERIDA_PRIVATE_KEY
ARG POLYGON_PRIVATE_KEY
ARG FAUCET_ENABLED=false
ARG FAUCET_URI=https://faucet-api.cheqd.network/credit
ARG FAUCET_DENOM=ncheq
ARG TESTNET_MINIMUM_BALANCE=50000000000

# Environment variables: base configuration
ENV NPM_CONFIG_LOGLEVEL ${NPM_CONFIG_LOGLEVEL}
Expand Down Expand Up @@ -106,6 +110,12 @@ ENV LOGTO_MANAGEMENT_API ${LOGTO_MANAGEMENT_API}
ENV LOGTO_DEFAULT_ROLE_ID ${LOGTO_DEFAULT_ROLE_ID}
ENV LOGTO_WEBHOOK_SECRET ${LOGTO_WEBHOOK_SECRET}

# Faucet setup
ENV FAUCET_ENABLED ${FAUCET_ENABLED}
ENV FAUCET_URI ${FAUCET_URI}
ENV FAUCET_DENOM ${FAUCET_DENOM}
ENV TESTNET_MINIMUM_BALANCE ${TESTNET_MINIMUM_BALANCE}

# Environment variables: Verida connector
ENV ENABLE_VERIDA_CONNECTOR ${ENABLE_VERIDA_CONNECTOR}
ENV VERIDA_NETWORK ${VERIDA_NETWORK}
Expand Down
6 changes: 6 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ LOGTO_DEFAULT_ROLE_ID="sdf...sdf"
LOGTO_WEBHOOK_SECRET="sdf...sdf"
COOKIE_SECRET="sdf...sdf"

# Faucet settings
FAUCET_ENABLED="false"
FAUCET_URI="https://faucet-api.cheqd.network/credit"
FAUCET_DENOM="ncheq"
TESTNET_MINIMUM_BALANCE="50000000000"

# Verida
ENABLE_VERIDA_CONNECTOR="false"
VERIDA_PRIVATE_KEY="akjvncanv....avoa"
Expand Down
10 changes: 6 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"README.md"
],
"dependencies": {
"@cheqd/sdk": "^3.6.1",
"@cheqd/did-provider-cheqd": "^3.6.1",
"@cosmjs/amino": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class App {
app.get(`/account`, new AccountController().get)

// LogTo webhooks
app.post(`/account/set-default-role`, LogToWebHook.verifyHookSignature, new AccountController().setupDefaultRole)
app.post(`/account/bootstrap`, LogToWebHook.verifyHookSignature, new AccountController().bootstrap)

// LogTo user info
app.get('/auth/user-info', async (req, res) => {
Expand Down
82 changes: 82 additions & 0 deletions src/controllers/customer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Request, Response } from 'express'
import { checkBalance } from '@cheqd/sdk'

import { CustomerService } from '../services/customer.js'
import { LogToHelper } from '../middleware/auth/logto.js'
import { FaucetHelper } from '../helpers/faucet.js'
import { StatusCodes } from 'http-status-codes'
import { LogToWebHook } from '../middleware/hook.js'

export class AccountController {

Expand Down Expand Up @@ -36,8 +39,17 @@ export class AccountController {
error: `Error creating customer. Please try again`
})
}
// Send some tokens for testnet
if (process.env.FAUCET_ENABLED === 'true') {
const resp = await FaucetHelper.delegateTokens(customer.address)
if (resp.status !== StatusCodes.OK) {
return response.status(resp.status).json({
error: resp.error})
}
}
return response.status(StatusCodes.OK).json({
customerId: customer.customerId,
address: customer.address,
})
} catch (error) {
return response.status(StatusCodes. INTERNAL_SERVER_ERROR).json({
Expand Down Expand Up @@ -106,4 +118,74 @@ export class AccountController {
}
return response.status(StatusCodes.BAD_REQUEST).json({})
}

public async bootstrap(request: Request, response: Response) {
// 1. Check that the customer exists
// 1.1 If not, create it
// 2. Get the user Roles
// 2.1 If list of roles is empty - assign default role
// 2.2 Create custom_data and update the userInfo (send it to the LogTo)
// 3. Check the token balance for Testnet account
// 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet
const customerId: string = response.locals.customerId || LogToWebHook.getCustomerId(request);
const logToHelper = new LogToHelper()
const _r = await logToHelper.setup()
if (_r.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: _r.error})
}
// 1. Check that the customer exists
let customer : any = await CustomerService.instance.get(customerId)
if (!customer) {
customer = await CustomerService.instance.create(customerId)
}
// 2. Get the user's roles
const roles = await logToHelper.getRolesForUser(customerId)
if (roles.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: roles.error})
}

// 2.1 If list of roles is empty and the user is not suspended - assign default role
if (roles.data.length === 0 && !LogToWebHook.isUserSuspended(request)) {
const _r = await logToHelper.setDefaultRoleForUser(customerId)
if (_r.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: _r.error})
}
}

const customDataFromLogTo = await logToHelper.getCustomData(customerId)

// 2.2 Create custom_data and update the userInfo (send it to the LogTo)
if (Object.keys(customDataFromLogTo.data).length === 0 && customer.address) {
const customData = {
cosmosAccounts: {
testnet: customer.address
}
}
const _r = await logToHelper.updateCustomData(customerId, customData)
if (_r.status !== 200) {
return response.status(_r.status).json({
error: _r.error})
}
}

// 3. Check the token balance for Testnet account
if (customer.address && process.env.FAUCET_ENABLED === 'true') {
const balances = await checkBalance(customer.address, process.env.TESTNET_RPC_URL)
if (balances.length === 0) {
const balance = balances[0]
if (!balance || +balance.amount < process.env.TESTNET_MINIMUM_BALANCE) {
// 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet
const resp = await FaucetHelper.delegateTokens(customer.address)
if (resp.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: resp.error})
}
}
}
}
return response.status(StatusCodes.OK).json({})
}
}
27 changes: 27 additions & 0 deletions src/helpers/faucet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ICommonErrorResponse } from "../types/authentication.js";
import { DEFAULT_FAUCET_DENOM, DEFAULT_FAUCET_URI } from "../types/constants.js";

export class FaucetHelper {

// ...
static async delegateTokens(address: string): Promise<ICommonErrorResponse> {
const faucetURI = DEFAULT_FAUCET_URI
const faucetBody = {
"denom": DEFAULT_FAUCET_DENOM,
"address": address,
}
const response = await fetch(faucetURI, {
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(faucetBody),
method: "POST"
});
return {
status: response.status,
error: await response.text(),
data: {}
}
}
// ...
}
5 changes: 0 additions & 5 deletions src/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,6 @@ export function generateDidDoc(options: IDidDocOptions) {
return createDidPayload(verificationMethods, [verificationKeys])
}

export function getCosmosAccount(kid: string) {
const { publicKeyConvert } = pkg
return toBech32('cheqd', rawSecp256k1PubkeyToRawAddress(publicKeyConvert(fromString(kid, 'hex'), true)))
}

export function verifyHookSignature(signingKey: string, rawBody: string, expectedSignature: string): boolean {
const hmac = createHmac('sha256', signingKey);
hmac.update(rawBody);
Expand Down
4 changes: 2 additions & 2 deletions src/middleware/auth/base-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
private static pathSkip = [
'/swagger',
'/static',
'/logto',
'/account/set-default-role',
'/logto',
'/account/bootstrap',
'/auth/user-info']

constructor () {
Expand Down
Loading

0 comments on commit d157535

Please sign in to comment.