Goal
Build a backend API for issuing SPL tokens to users and tracking the outstanding debt
Requirements
- User can request a token amount to be lent over by the server and receive the requested amount on his solana wallet
- User can view his outstanding debt (wallet address, amount, token address)
- User can view all outstanding debts (wallet address, amount, token address)
Tech stack
- Postgres
- Docker
- Python/Nodejs/JVM-based languages
Bonus points:
- Verify that the user requesting tokens is the owner of the keypair
- Deploying and providing API scripts that test the flow and state
Hints
- use devnet cluster for building and showcase
- create test token and support only that specific token to reduce scope
Terminology
SPL - solana program libarary - official token specification of Solana chain
Lending - server issues tokens and keeps tracking of the amounts
Useful references
Solana RPC api for creating transactions: https://docs.solana.com/developing/clients/jsonrpc-api
Solana SPL token docs for token transfer instruction:
- https://spl.solana.com/token
- https://www.npmjs.com/package/@solana/spl-token
- https://dev.to/0xbolt/how-to-send-solana-transaction-using-python-1dii
Solana transaction explorer: https://explorer.solana.com/
A backend API service was written in Python language. A backend API stores users' loans in postgres database and allows to initialize the loan by provided public key for a specified amount, then user has to approve the loan by signing received loan id with hist keypair and posting the signature back to API.
Also, users may get a list of all loans the service provided and filter the necessary information from API.
A detailed openapi styled documentation is available on http://localhost:8000/docs url (to visit it, you have to start the API service on your local machine).
Project:
- docker + docker-compose
- python
- poetry -- manages python project dependencies
- FastAPI + uvicorn -- API is build on it and server runs with uvicorn
- pydantic -- provides serialization for API and token configuration for the service
- solana -- provides a simplified integration with solana / SPL json-rpc API
- gino + alembic -- performs postgres SQL query execution and DB migrations
- dependency-injector -- performs application assembling and easy app configuration in tests
- mypy + pytest -- code type checking & integration tests were implemented with these tools
- postgres
- sometimes solana devnet returns 429 (Too Many Requests) or even Internal Server Error, so client has to wait awhile
and retry the operation
- usually this happens when server initializes a new wallet with airdrop and creates a new token and mints some amount of it
- on first token lending service will initialize wallet and token account, so request duration may take up to 2 minutes
- submit loan request may take up to 1 minute, because service waits for token transfer transaction to be finalized
Start your own centralized API for token lending on your local machine using docker-compose
# build spl-token-lending-api service docker image
docker-compose build
# run spl token lending project with postgres and spl-token-lending-api service
docker-compose up -d
Use CLI script for initializing your custom wallet and lend some tokens
python scripts/token-lending-cli.py lend
user keypair: ******
user pubkey: HXJ9DuvFSqfrUPxoytns3zybYjMHjzrvsGMtc45mUujf
loan amount: 133
loan initialized: {'id': '******', 'status': 'PENDING', 'wallet': 'HXJ9DuvFSqfrUPxoytns3zybYjMHjzrvsGMtc45mUujf', 'amount': 133}
submit [y/n]: y
confirming the loan with keypair signature (this can take a while) ...
loan submitted: {'id': '******', 'status': 'ACTIVE', 'wallet': 'HXJ9DuvFSqfrUPxoytns3zybYjMHjzrvsGMtc45mUujf', 'amount': 133}
List your loans
python scripts/token-lending-cli.py list
user keypair: ******
user pubkey: HXJ9DuvFSqfrUPxoytns3zybYjMHjzrvsGMtc45mUujf
loans: {'info': {'offset': 0, 'limit': 1000, 'total': 1}, 'items': [{'id': '******', 'status': 'ACTIVE', 'wallet': 'HXJ9DuvFSqfrUPxoytns3zybYjMHjzrvsGMtc45mUujf', 'amount': 133}]}
You may find your lent tokens in solana explorer: https://explorer.solana.com/address/HXJ9DuvFSqfrUPxoytns3zybYjMHjzrvsGMtc45mUujf/tokens?cluster=devnet
Install poetry, python, docker, docker-compose.
Setup project for development (all following commands assumed to be run from project root dir).
poetry install
Create dev files: .env
and docker-compose.override.yml
# .env content example
TOKEN_REPOSITORY_CONFIG_PATH=tests/secrets/token-repository-config.json
SOLANA_ENDPOINT=https://api.devnet.solana.com
POSTGRES_DSN=postgresql://spl-token-lending:secret@localhost:5432/dev
POSTGRES_PASSWORD=secret
LOGGING_LEVEL=debug
# docker-compose.override.yml content example
version: '3.3'
services:
# map postgres ports, so you can connect to DB in tests / local service run
postgres:
ports:
- "5432:5432"
Run MyPy check
poetry run mypy
Run tests
export CONFIG_ENV_FILE=.env
# run all tests, except slow (for quick checks, no coverage reports + enable debugger breakpoints and stop on first test failure)
poetry run pytest --no-cov -x -m "not slow"
# run all tests, this won't be fast
poetry run pytest
- Simplify lending process:
- Make single endpoint for accepting the loan request from a user
- Create a loan in DB and start a token transfer transaction with 2 signatures: token lending source (service) & token destination (user)
- Service should start to listen for user to sign / confirm the transaction (try to use websocket API for that)
- After user signs the transaction with his own keypair - solana finishes transaction and service updates the status of the loan to active
- Reduce loan amount when user returns some tokens to source account and close loans
- Listen to source account token transfers and analyse incoming tokens and reduce loan amount for appropriate wallet
- Close the loan when user returns full amount
- Store source token account configuration in database, not in a config file.
- Add endpoints to initialize the wallet / tokens and store info in DB.
- Construct token lending entity in the code from database when loan initializes
- (Best solution for cases 1, 2, 3) refactor API, so it just subscribes and listens for specified token transfers and calculates loans
- Increase test coverage, test code for solana integration failures and make tests more reproducible (use fixed values for public keys and keypairs).