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

feat: add cli tool to interact with nano community API #114

Merged
merged 14 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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
Loading