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 api endpoint for signed message #110

Merged
merged 2 commits into from
Apr 9, 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
11 changes: 11 additions & 0 deletions api/routes/auth/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from 'express'

import register from './register.mjs'
import message from './message.mjs'

const router = express.Router()

router.use('/register', register)
router.use('/message', message)

export default router
198 changes: 198 additions & 0 deletions api/routes/auth/message.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import express from 'express'
import { tools } from 'nanocurrency-web'
import BigNumber from 'bignumber.js'

import { rpc, verify_nano_community_message_signature } from '#common'
import {
ACCOUNT_TRACKING_MINIMUM_BALANCE,
REPRESENTATIVE_TRACKING_MINIMUM_VOTING_WEIGHT
} from '#constants'

const router = express.Router()

router.post('/?', async (req, res) => {
const { logger, db } = req.app.locals
try {
const { message } = req.body

const {
version,

entry_id,
chain_id,
entry_clock,
chain_clock,

public_key,
operation,
content,
tags,

references,

created_at,

signature
} = message

if (version !== 1) {
return res.status(400).send('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')
}

// chain_id must be null or 32 byte hash
if (chain_id && chain_id.length !== 64) {
return res.status(400).send('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')
}

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

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

// operation must be SET or DELETE
if (operation !== 'SET' && operation !== 'DELETE') {
return res.status(400).send('Invalid operation')
}

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

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

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

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

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

// validate signature
const is_valid_signature = verify_nano_community_message_signature({
entry_id,
chain_id,
entry_clock,
chain_clock,
public_key,
operation,
content,
tags,
references,
created_at,
signature
})
if (!is_valid_signature) {
return res.status(400).send('Invalid signature')
}

// public_key can be a linked keypair or an existing nano account

const linked_accounts = await db('user_addresses')
.select('user_addresses.address')
.leftJoin('users', 'users.id', '=', 'user_addresses.user_id')
.where({ public_key })

const nano_account = tools.publicKeyToAddress(public_key)

const all_accounts = [
...linked_accounts.map((row) => row.address),
nano_account
]

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

// 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)
)

// 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
)
)

if (has_balance || has_weight) {
await db('nano_community_messages')
.insert({
version,

entry_id,
chain_id,
entry_clock,
chain_clock,

public_key,
operation,
content,
tags: tags.length > 0 ? tags.join(', ') : null,

signature,

references: references.length > 0 ? references.join(', ') : null,

created_at
})
.onConflict()
.merge()
}

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

entry_id,
chain_id,
entry_clock,
chain_clock,

public_key,
operation,
content,
tags,

references,

created_at
})
} catch (error) {
console.log(error)
logger(error)
res.status(500).send('Internal server error')
}
})

export default router
17 changes: 9 additions & 8 deletions api/routes/auth.mjs → api/routes/auth/register.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { tools } from 'nanocurrency-web'
const router = express.Router()
const USERNAME_RE = /^[A-Za-z][a-zA-Z0-9_]+$/

router.post('/register', async (req, res) => {
router.post('/?', async (req, res) => {
const { logger, db } = req.app.locals
try {
// public_key is a linked keypair for the given address
const required = ['public_key', 'address', 'signature', 'username']
for (const prop of required) {
if (!req.body[prop]) {
Expand Down Expand Up @@ -48,9 +49,9 @@ router.post('/register', async (req, res) => {
address = address.replace('xrb_', 'nano_')

const exists = await db('user_addresses').where({ address })
let accountId = exists.length ? exists[0].account_id : null
let user_id = exists.length ? exists[0].user_id : null

if (!accountId) {
if (!user_id) {
const result = await db('users')
.insert({
public_key,
Expand All @@ -59,31 +60,31 @@ router.post('/register', async (req, res) => {
})
.onConflict()
.merge()
accountId = result[0]
user_id = result[0]
} else {
await db('users')
.update({ last_visit: Math.round(Date.now() / 1000), username })
.where({ id: accountId })
.where({ id: user_id })
}

await db('user_addresses')
.insert({
account_id: accountId,
user_id,
address,
signature
})
.onConflict()
.merge()

return res.send({
accountId,
user_id,
address,
username,
signature
})
} catch (error) {
logger(error)
res.status(500).send({ error: error.toString() })
res.status(500).send('Internal server error')
}
})

Expand Down
2 changes: 1 addition & 1 deletion api/routes/index.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default as auth } from './auth.mjs'
export { default as auth } from './auth/index.mjs'
export { default as accounts } from './accounts/index.mjs'
export { default as blocks } from './blocks.mjs'
export { default as posts } from './posts.mjs'
Expand Down
10 changes: 9 additions & 1 deletion api/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import serveStatic from 'serve-static'
import cors from 'cors'
import favicon from 'express-favicon'
import robots from 'express-robots-txt'
import { slowDown } from 'express-slow-down'

import config from '#config'
import * as routes from '#api/routes/index.mjs'
Expand All @@ -33,6 +34,12 @@ const logger = debug('api')
const defaults = {}
const options = extend(defaults, config)
const IS_DEV = process.env.NODE_ENV === 'development'
const speedLimiter = slowDown({
windowMs: 10 * 60 * 1000, // 10 minutes
delayAfter: 50, // allow 50 requests per 10 minutes, then...
delayMs: (hits, req) => (hits - req.slowDown.limit) * 500, // begin adding 500ms of delay per request above 50:
maxDelayMs: 20000 // maximum delay of 20 seconds
})

const api = express()

Expand Down Expand Up @@ -88,13 +95,14 @@ api.use('/api/nanodb-experimental', routes.nanodb_experimental)
api.use('/api/posts', routes.posts)
api.use('/api/network', routes.network)
api.use('/api/github', routes.github)
api.use('/api/auth', routes.auth)
api.use('/api/auth', speedLimiter, routes.auth)
api.use('/api/accounts', routes.accounts)
api.use('/api/blocks', routes.blocks)
api.use('/api/representatives', routes.representatives)
api.use('/api/weight', routes.weight)

const docsPath = path.join(__dirname, '..', 'docs')

api.use('/api/docs', serveStatic(docsPath))
api.get('/api/docs/*', (req, res) => {
res.status(404).send('Not found')
Expand Down
5 changes: 5 additions & 0 deletions common/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * as cloudflare from './cloudflare.mjs'
export { request }
export { default as convertToCSV } from './convert-to-csv.mjs'
export { default as read_csv } from './read-csv.mjs'
export { default as verify_nano_community_message_signature } from './verify-nano-community-message-signature.mjs'
export { default as sign_nano_community_message } from './sign-nano-community-message.mjs'

const POST = (data) => ({
method: 'POST',
Expand Down Expand Up @@ -83,6 +85,9 @@ const rpcRequest = async (
{ url, trusted = false, timeout = 20000 } = {}
) => {
if (url) {
console.log({
url
})
const options = { url, timeout, ...POST(data) }
return request(options)
}
Expand Down
33 changes: 33 additions & 0 deletions common/sign-nano-community-message.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import ed25519 from '@trashman/ed25519-blake2b'

export default function sign_nano_community_message(message, private_key) {
const {
entry_id,
chain_id,
entry_clock,
chain_clock,
public_key,
operation,
content,
tags,
references,
created_at
} = message

const data = Buffer.from([
entry_id,
chain_id,
entry_clock,
chain_clock,
public_key,
operation,
content,
tags,
references,
created_at
])

const message_hash = ed25519.hash(data)

return ed25519.sign(message_hash, private_key, public_key)
}
Loading
Loading