Skip to content

Commit

Permalink
feat: add cli tool to interact with nano community API (#114)
Browse files Browse the repository at this point in the history
* feat: add cli tool to interact with nano community API

* chore: add cli test

* chore: cleanup logging

* chore: cleanup help formatting and console logging

* chore: remove dependencies

* feat: allow revoking linked signing key with the signing key or linked account key

* feat: process signed messages

* docs: added `docs/cli.md` doc

* chore: setup package.json for cli

* chore: make `cli/index.mjs` executable

* docs: update `docs/cli.md` installation section

* docs: update `docs/cli.md`

* fix: show help when no arguements passed

* fix: github test workflow
  • Loading branch information
mistakia authored Jun 18, 2024
1 parent 9beecae commit a76b611
Show file tree
Hide file tree
Showing 28 changed files with 3,425 additions and 275 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ jobs:
- name: yarn install
run: |
YARN_CHECKSUM_BEHAVIOR=update yarn install
cd cli
YARN_CHECKSUM_BEHAVIOR=update yarn install
cd ..
- name: yarn lint
env:
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ yarn-error.log
!.yarn/releases
!.yarn/sdks
!.yarn/versions
cli/.pnp.*
cli/.yarn/*
!cli/.yarn/patches
!cli/.yarn/plugins
!cli/.yarn/releases
!cli/.yarn/sdks
!cli/.yarn/versions

# App specific
#
Expand Down
91 changes: 53 additions & 38 deletions api/routes/auth/message.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import express from 'express'
import { tools } from 'nanocurrency-web'
import BigNumber from 'bignumber.js'

import { rpc, verify_nano_community_message_signature } from '#common'
import {
rpc,
verify_nano_community_message_signature,
encode_nano_address
} from '#common'
import {
ACCOUNT_TRACKING_MINIMUM_BALANCE,
REPRESENTATIVE_TRACKING_MINIMUM_VOTING_WEIGHT
} from '#constants'
import { process_community_message } from '#libs-server'

const router = express.Router()

Expand All @@ -26,72 +30,78 @@ router.post('/?', async (req, res) => {
public_key,
operation,
content,
tags,
tags = [],

references,
references = [],

created_at,

signature
} = message

if (version !== 1) {
return res.status(400).send('Invalid message version')
return res.status(400).json({ error: 'Invalid message version' })
}

// entry_id must be null or 32 byte hash
if (entry_id && entry_id.length !== 64) {
return res.status(400).send('Invalid entry_id')
return res.status(400).json({ error: 'Invalid entry_id' })
}

// chain_id must be null or 32 byte hash
if (chain_id && chain_id.length !== 64) {
return res.status(400).send('Invalid chain_id')
return res.status(400).json({ error: 'Invalid chain_id' })
}

// entry_clock must be null or positive integer
if (entry_clock && entry_clock < 0) {
return res.status(400).send('Invalid entry_clock')
return res.status(400).json({ error: 'Invalid entry_clock' })
}

// chain_clock must be null or positive integer
if (chain_clock && chain_clock < 0) {
return res.status(400).send('Invalid chain_clock')
return res.status(400).json({ error: 'Invalid chain_clock' })
}

// public_key must be 32 byte hash
if (public_key.length !== 64) {
return res.status(400).send('Invalid public_key')
return res.status(400).json({ error: 'Invalid public_key' })
}

// operation must be SET or DELETE
if (operation !== 'SET' && operation !== 'DELETE') {
return res.status(400).send('Invalid operation')
const allowed_operations = [
'SET',
'SET_ACCOUNT_META',
'SET_REPRESENTATIVE_META',
'SET_BLOCK_META'
]
if (!allowed_operations.includes(operation)) {
return res.status(400).json({ error: 'Invalid operation' })
}

// content must be null or string
if (content && typeof content !== 'string') {
return res.status(400).send('Invalid content')
return res.status(400).json({ error: 'Invalid content' })
}

// tags must be null or array of strings
if (tags && !Array.isArray(tags)) {
return res.status(400).send('Invalid tags')
return res.status(400).json({ error: 'Invalid tags' })
}

// references must be null or array of strings
if (references && !Array.isArray(references)) {
return res.status(400).send('Invalid references')
return res.status(400).json({ error: 'Invalid references' })
}

// created_at must be null or positive integer
if (created_at && created_at < 0) {
return res.status(400).send('Invalid created_at')
return res.status(400).json({ error: 'Invalid created_at' })
}

// signature must be 64 byte hash
if (signature.length !== 128) {
return res.status(400).send('Invalid signature')
return res.status(400).json({ error: 'Invalid signature' })
}

// validate signature
Expand All @@ -109,39 +119,35 @@ router.post('/?', async (req, res) => {
signature
})
if (!is_valid_signature) {
return res.status(400).send('Invalid signature')
return res.status(400).json({ error: 'Invalid signature' })
}

// public_key can be a linked keypair or an existing nano account
const linked_accounts = await db('account_keys')

const linked_account = await db('account_keys')
.select('account')
.where({ public_key })
.whereNull('revoked_at')
const nano_account = tools.publicKeyToAddress(public_key)
.first()

const all_accounts = [
...linked_accounts.map((row) => row.account),
nano_account
]
const message_nano_account = linked_account
? linked_account.account
: encode_nano_address({
public_key_buf: Buffer.from(public_key, 'hex')
})

const accounts_info = []
for (const account of all_accounts) {
const account_info = await rpc.accountInfo({ account })
if (account_info) {
accounts_info.push(account_info)
}
}
const account_info = await rpc.accountInfo({
account: message_nano_account
})

// check if any of the accounts have a balance beyond the tracking threshold
const has_balance = accounts_info.some((account_info) =>
new BigNumber(account_info.balance).gte(ACCOUNT_TRACKING_MINIMUM_BALANCE)
const has_balance = new BigNumber(account_info?.balance || 0).gte(
ACCOUNT_TRACKING_MINIMUM_BALANCE
)

// check if any of the accounts have weight beyond the tracking threshold
const has_weight = accounts_info.some((account_info) =>
new BigNumber(account_info.weight).gte(
REPRESENTATIVE_TRACKING_MINIMUM_VOTING_WEIGHT
)
const has_weight = new BigNumber(account_info?.weight || 0).gte(
REPRESENTATIVE_TRACKING_MINIMUM_VOTING_WEIGHT
)

if (has_balance || has_weight) {
Expand Down Expand Up @@ -169,6 +175,15 @@ router.post('/?', async (req, res) => {
.merge()
}

try {
await process_community_message({
message,
message_account: message_nano_account
})
} catch (error) {
logger(error)
}

res.status(200).send({
version,

Expand All @@ -189,7 +204,7 @@ router.post('/?', async (req, res) => {
} catch (error) {
console.log(error)
logger(error)
res.status(500).send('Internal server error')
res.status(500).json({ error: 'Internal server error' })
}
})

Expand Down
23 changes: 15 additions & 8 deletions api/routes/auth/register.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import express from 'express'
import nano from 'nanocurrency'
import ed25519 from '@trashman/ed25519-blake2b'

import { verify_nano_community_link_key_signature } from '#common'
import {
verify_nano_community_link_key_signature,
is_nano_address_valid,
decode_nano_address
} from '#common'

const router = express.Router()
const USERNAME_RE = /^[A-Za-z][a-zA-Z0-9_]+$/
const PUBLIC_KEY_RE = /^[0-9a-fA-F]{64}$/
const SIGNATURE_RE = /^[0-9a-fA-F]{128}$/

router.post('/?', async (req, res) => {
const { logger, db } = req.app.locals
Expand All @@ -19,15 +24,15 @@ router.post('/?', async (req, res) => {

const { public_key, signature, username } = req.body

if (!nano.checkKey(public_key)) {
if (typeof public_key !== 'string' || !PUBLIC_KEY_RE.test(public_key)) {
return res.status(401).send({ error: 'invalid public_key param' })
}

if (!USERNAME_RE.test(username)) {
return res.status(401).send({ error: 'invalid username param' })
}

if (!nano.checkSignature(signature)) {
if (typeof signature !== 'string' || !SIGNATURE_RE.test(signature)) {
return res.status(401).send({ error: 'invalid signature' })
}

Expand Down Expand Up @@ -78,19 +83,21 @@ router.post('/key/?', async (req, res) => {

const { public_key, signature, account } = req.body

if (!nano.checkKey(public_key)) {
if (typeof public_key !== 'string' || !PUBLIC_KEY_RE.test(public_key)) {
return res.status(401).send({ error: 'invalid public_key param' })
}

if (!nano.checkAddress(account)) {
if (!is_nano_address_valid(account)) {
return res.status(401).send({ error: 'invalid account param' })
}

if (!nano.checkSignature(signature)) {
if (typeof signature !== 'string' || !SIGNATURE_RE.test(signature)) {
return res.status(401).send({ error: 'invalid signature' })
}

const account_public_key = nano.derivePublicKey(account)
const { public_key: account_public_key } = decode_nano_address({
address: account
})
const valid_signature = verify_nano_community_link_key_signature({
linked_public_key: public_key,
nano_account: account,
Expand Down
65 changes: 35 additions & 30 deletions api/routes/auth/revoke.mjs
Original file line number Diff line number Diff line change
@@ -1,52 +1,37 @@
import express from 'express'
import nano from 'nanocurrency'
import { verify_nano_community_revoke_key_signature } from '#common'
import {
verify_nano_community_revoke_key_signature,
decode_nano_address
} from '#common'

const router = express.Router()
const PUBLIC_KEY_RE = /^[0-9a-fA-F]{64}$/
const SIGNATURE_RE = /^[0-9a-fA-F]{128}$/

router.post('/key/?', async (req, res) => {
const { logger, db } = req.app.locals
try {
const required = ['account', 'public_key', 'signature']
const required = ['public_key', 'signature']
for (const prop of required) {
if (!req.body[prop]) {
return res.status(400).send({ error: `missing ${prop} param` })
}
}

const { account, public_key, signature } = req.body
const { public_key, signature } = req.body

if (!nano.checkAddress(account)) {
return res.status(401).send({ error: 'invalid account param' })
}

if (!nano.checkKey(public_key)) {
if (typeof public_key !== 'string' || !PUBLIC_KEY_RE.test(public_key)) {
return res.status(401).send({ error: 'invalid public_key param' })
}

if (!nano.checkSignature(signature)) {
return res.status(401).send({ error: 'invalid signature' })
}

const account_public_key = nano.derivePublicKey(account)
const valid_signature = verify_nano_community_revoke_key_signature({
linked_public_key: public_key,
nano_account: account,
nano_account_public_key: account_public_key,
signature
})
if (!valid_signature) {
return res.status(401).send({ error: 'invalid signature' })
if (typeof signature !== 'string' || !SIGNATURE_RE.test(signature)) {
return res.status(401).send({ error: 'invalid signature param' })
}

const linked_key = await db('account_keys')
.where({ account, public_key })
.first()
const linked_key = await db('account_keys').where({ public_key }).first()

if (!linked_key) {
return res
.status(401)
.send({ error: `key ${public_key} not linked to account ${account}` })
return res.status(401).send({ error: `key ${public_key} not found` })
}

if (linked_key.revoked_at) {
Expand All @@ -55,13 +40,33 @@ router.post('/key/?', async (req, res) => {
.send({ error: `key ${public_key} already revoked` })
}

const valid_signing_key_signature =
verify_nano_community_revoke_key_signature({
linked_public_key: public_key,
either_public_key: public_key,
signature
})
const { public_key: account_public_key } = decode_nano_address({
address: linked_key.account
})
const valid_account_key_signature =
verify_nano_community_revoke_key_signature({
linked_public_key: public_key,
either_public_key: account_public_key,
signature
})

if (!valid_signing_key_signature && !valid_account_key_signature) {
return res.status(401).send({ error: 'invalid signature' })
}

const revoked_at = Math.floor(Date.now() / 1000)
await db('account_keys')
.update({ revoked_at, revoke_signature: signature })
.where({ account, public_key })
.where({ account: linked_key.account, public_key })

res.status(200).send({
account,
account: linked_key.account,
public_key,
signature,
created_at: linked_key.created_at,
Expand Down
Loading

0 comments on commit a76b611

Please sign in to comment.