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: added getAddressAtIndex method and API #425

Merged
merged 12 commits into from
Jun 6, 2023
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
7 changes: 7 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
if [[ $(type -t use_flake) != function ]]; then
echo "ERROR: use_flake function missing."
echo "Please update direnv to v2.30.0 or later."
exit 1
fi

use flake
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ coverage

# fcm account config
**/fcm.config.json

.direnv
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,16 @@ Sometimes, jest will use old cached js files, even after you modified the typesc
## Standard Operating Procedures

Check it in [docs/SOP.md](docs/SOP.md)


## Nix flakes

## Using this project

This project uses [Nix](https://nixos.org/) with [direnv](https://direnv.net/) to help with dependencies, including Node.js. To get started, you need to have Nix and direnv installed.

1. Install [Nix](https://nixos.org/download.html) and [Direnv](https://direnv.net/docs/installation.html).
2. Enable flake support in Nix: `nix-env -iA nixpkgs.nixUnstable`
3. Allow direnv to work in your shell by running `direnv allow`

Now, every time you enter the project directory, direnv will automatically activate the environment from flake.nix, including the specific version of Node.js specified there. When you leave the directory, it will deactivate. This ensures a consistent and isolated environment per project.
17 changes: 17 additions & 0 deletions db/migrations/20230601151507-address_index_idx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

module.exports = {
up: async (queryInterface) => {
await queryInterface.addIndex(
'address',
['index'], {
name: 'address_index_idx',
fields: 'index',
},
);
},

down: async (queryInterface) => {
await queryInterface.removeIndex('address', 'address_index_idx');
},
};
92 changes: 92 additions & 0 deletions flake.lock

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

39 changes: 39 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
This flake.nix file creates a virtual environment with the desired dependencies
in a reproducible way using Nix and Nix flakes (https://nixos.wiki/wiki/Flakes)

Flakes are a feature in Nix that allows you to specify the dependencies of your
project in a declarative and reproducible manner. It allows for better isolation,
reproducibility, and more reliable upgrades.

`direnv` is an environment switcher for the shell. It knows how to hook into
multiple shells (like bash, zsh, fish, etc...) to load or unload environment
variables depending on the current directory. This allows project-specific
environment variables without cluttering the "~/.profile" file.

This flake file creates a shell with nodejs v14.x installed and should work
on macOs, linux and windows
*/
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this file accepts comments, I think we should explain what this is.

Maybe also mention in the README (or some other doc that we would link in the README) that we have these configs and how to use them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5c9ab39

description = "Flake that installs Node.js 14.x via direnv";

inputs.devshell.url = "github:numtide/devshell";
inputs.flake-utils.url = "github:numtide/flake-utils";

outputs = { self, flake-utils, devshell, nixpkgs }:

flake-utils.lib.eachDefaultSystem (system: {
devShell =
let pkgs = import nixpkgs {
inherit system;

overlays = [ devshell.overlay ];
};
in
pkgs.devshell.mkShell {
packages = with pkgs; [
nodejs-14_x
];
};
});
}
4 changes: 4 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ functions:
method: get
cors: true
authorizer: ${self:custom.authorizer.walletBearer}
request:
parameters:
paths:
index: false
warmup:
walletWarmer:
enabled: true
Expand Down
89 changes: 71 additions & 18 deletions src/api/addresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@

import 'source-map-support/register';

import Joi from 'joi';
import Joi, { ValidationError } from 'joi';
import { APIGatewayProxyHandler } from 'aws-lambda';
import { ApiError } from '@src/api/errors';
import { closeDbAndGetError, warmupMiddleware } from '@src/api/utils';
import {
getWallet,
getWalletAddresses,
getAddressAtIndex as dbGetAddressAtIndex,
} from '@src/db';
import { AddressInfo } from '@src/types';
import { AddressInfo, AddressAtIndexRequest } from '@src/types';
import { closeDbConnection, getDbConnection } from '@src/utils';
import { walletIdProxyHandler } from '@src/commons';
import middy from '@middy/core';
Expand All @@ -32,6 +33,19 @@ const checkMineBodySchema = Joi.object({
.required(),
});

class AddressAtIndexValidator {
static readonly bodySchema = Joi.object({
index: Joi.number().min(0).optional(),
});

static validate(payload: unknown): { value: AddressAtIndexRequest, error: ValidationError} {
return AddressAtIndexValidator.bodySchema.validate(payload, {
abortEarly: false, // We want it to return all the errors not only the first
convert: true, // We need to convert as parameters are sent on the QueryString
}) as { value: AddressAtIndexRequest, error: ValidationError };
}
}

/*
* Check if a list of addresses belong to the caller wallet
*
Expand Down Expand Up @@ -93,27 +107,66 @@ export const checkMine: APIGatewayProxyHandler = middy(walletIdProxyHandler(asyn
})).use(cors());

/*
* Get the addresses of a wallet
* Get the addresses of a wallet, allowing an index filter
* Notice: If the index filter is passed, it will only find addresses
* that are already in our database, this will not derive new addresses
*
* This lambda is called by API Gateway on GET /addresses
*/
export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (walletId) => {
const status = await getWallet(mysql, walletId);
export const get: APIGatewayProxyHandler = middy(
walletIdProxyHandler(async (walletId, event) => {
const status = await getWallet(mysql, walletId);

if (!status) {
return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND);
}
if (!status.readyAt) {
return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY);
}
if (!status) {
return closeDbAndGetError(mysql, ApiError.WALLET_NOT_FOUND);
}

const addresses = await getWalletAddresses(mysql, walletId);
if (!status.readyAt) {
return closeDbAndGetError(mysql, ApiError.WALLET_NOT_READY);
}

await closeDbConnection(mysql);
const { value: body, error } = AddressAtIndexValidator.validate(event.pathParameters);

return {
statusCode: 200,
body: JSON.stringify({ success: true, addresses }),
};
})).use(cors())
if (error) {
const details = error.details.map((err) => ({
message: err.message,
path: err.path,
}));

return closeDbAndGetError(mysql, ApiError.INVALID_PAYLOAD, { details });
}

let response = null;

if ('index' in body) {
const address: AddressInfo | null = await dbGetAddressAtIndex(mysql, walletId, body.index);

if (!address) {
return closeDbAndGetError(mysql, ApiError.ADDRESS_NOT_FOUND);
}

response = {
statusCode: 200,
body: JSON.stringify({
success: true,
addresses: [address],
}),
};
} else {
// Searching for multiple addresses
const addresses = await getWalletAddresses(mysql, walletId);
response = {
statusCode: 200,
body: JSON.stringify({
success: true,
addresses,
}),
};
}

await closeDbConnection(mysql);

return response;
}),
).use(cors())
.use(warmupMiddleware());
1 change: 1 addition & 0 deletions src/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum ApiError {
WALLET_ALREADY_LOADED = 'wallet-already-loaded',
WALLET_MAX_RETRIES = 'wallet-max-retries',
ADDRESS_NOT_IN_WALLET = 'address-not-in-wallet',
ADDRESS_NOT_FOUND = 'address-not-found',
TX_OUTPUT_NOT_IN_WALLET = 'tx-output-not-in-wallet',
TOKEN_NOT_FOUND = 'token-not-found',
FORBIDDEN = 'forbidden',
Expand Down
1 change: 1 addition & 0 deletions src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const STATUS_CODE_TABLE = {
[ApiError.TOKEN_NOT_FOUND]: 404,
[ApiError.DEVICE_NOT_FOUND]: 404,
[ApiError.TX_NOT_FOUND]: 404,
[ApiError.ADDRESS_NOT_FOUND]: 404,
};

/**
Expand Down
35 changes: 35 additions & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3157,3 +3157,38 @@ export const getUnsentTxProposals = async (

return result.map((row) => row.id);
};

/**
* Gets a specific address from an index and a walletId
*
* @param mysql - Database connection
* @param walletId - The wallet id to search for
* @param index - The address index to search for
*
* @returns An object containing the address, its index and the number of transactions
*/
export const getAddressAtIndex = async (
mysql: ServerlessMysql,
walletId: string,
index: number,
): Promise<AddressInfo | null> => {
const addresses = await mysql.query<AddressInfo[]>(
`
SELECT \`address\`, \`index\`, \`transactions\`
FROM \`address\` pd
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For an address to be available in this table, we need to have had a transaction involving the address, right?

I'm asking this because, if this is true, then this new API you're building won't be able to actually derive a new address at an index, right?

I don't know how this API will be used to evaluate whether this is a limitation or not

Copy link
Collaborator Author

@andreabadesso andreabadesso Jun 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're partially correct

We also make sure that the wallet always have at least MAX_GAP addresses starting from the last used index

So you're correct that this API won't be able to actually derive a new address at an index, but that's the expected behavior since this is exactly what the old facade does, so this is definetly a limitation but is not a problem for our use case

I added a notice in the API docs in 724fde2

WHERE \`index\` = ?
AND \`wallet_id\` = ?
LIMIT 1`,
[walletId, index],
);

if (addresses.length <= 0) {
return null;
}

return {
address: addresses[0].address as string,
index: addresses[0].index as number,
transactions: addresses[0].transactions as number,
} as AddressInfo;
};
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,10 @@ export interface PushDelete {
deviceId: string,
}

export interface AddressAtIndexRequest {
index?: number,
}

export interface TxByIdRequest {
txId: string,
}
Expand Down
Loading