SPL token lending scaffolding (#582)

* Scaffolding for spl-token-lending program

* Scaffolding for TS client
This commit is contained in:
Justin Starry 2020-10-09 13:29:51 +08:00 committed by GitHub
parent 5bd2e1c98c
commit a4f09fe05e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 3391 additions and 2 deletions

View File

@ -9,11 +9,20 @@ updates:
open-pull-requests-limit: 3
labels:
- "automerge"
- package-ecosystem: npm
directory: "/token-lending/js"
schedule:
interval: daily
time: "02:00"
timezone: America/Los_Angeles
open-pull-requests-limit: 3
labels:
- "automerge"
- package-ecosystem: npm
directory: "/token-swap/js"
schedule:
interval: daily
time: "01:00"
time: "03:00"
timezone: America/Los_Angeles
open-pull-requests-limit: 3
labels:
@ -22,7 +31,7 @@ updates:
directory: "/"
schedule:
interval: daily
time: "01:00"
time: "04:00"
timezone: America/Los_Angeles
labels:
- "automerge"

13
Cargo.lock generated
View File

@ -2846,6 +2846,19 @@ dependencies = [
"spl-token 2.0.6",
]
[[package]]
name = "spl-token-lending"
version = "0.1.0"
dependencies = [
"arrayref",
"num-derive",
"num-traits",
"num_enum",
"rand",
"solana-sdk",
"thiserror",
]
[[package]]
name = "spl-token-swap"
version = "0.1.0"

View File

@ -5,6 +5,7 @@ members = [
"memo/program",
"themis/program_bn",
"themis/program_ristretto",
"token-lending/program",
"token-swap/program",
"token/cli",
"token/program",

View File

@ -106,4 +106,20 @@ js_token_swap() {
}
_ js_token_swap
# Test token-lending js bindings
js_token_lending() {
cd token-lending/js
time npm install || exit $?
time npm run lint || exit $?
time npm run build || exit $?
npm run cluster:localnet || exit $?
npm run localnet:down
npm run localnet:update || exit $?
npm run localnet:up || exit $?
time npm run start || exit $?
npm run localnet:down
}
_ js_token_lending
exit 0

View File

@ -23,6 +23,7 @@ if [[ -z $1 ]]; then
programs=(
memo/program
token/program
token-lending/program
token-swap/program
)
else

7
token-lending/README.md Normal file
View File

@ -0,0 +1,7 @@
# Lending program
A lending protocol for the Token program on the Solana blockchain inspired by Aave.
Full documentation will be made available at https://spl.solana.com in the future
Web3 bindings are available in the `./js` directory.

View File

@ -0,0 +1,35 @@
{
"root": true,
"env": {
"node": true,
"browser": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier",
"prettier/@typescript-eslint"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"parser": "babel-eslint",
"project": "./tsconfig.json",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"prettier"
],
"rules": {
"no-console": 0,
"semi": 0,
"template-curly-spacing": [
2,
"always"
],
"@typescript-eslint/no-explicit-any": 0
}
}

13
token-lending/js/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
node_modules
coverage
.nyc_output
.DS_Store
*.log
.vscode
.idea
dist
compiled
.awcache
.rpt2_cache
docs
lib

View File

@ -0,0 +1,69 @@
# Token-lending JavaScript API
The Token-lending JavaScript library comprises:
* A library to interact with the on-chain program
* A test client that exercises the program
* Scripts to facilitate building the program
## Getting Started
First fetch the npm dependencies, including `@solana/web3.js`, by running:
```sh
$ npm install
```
### Select a Network
The client connects to a local Solana cluster by default.
To enable on-chain program logs, set the `RUST_LOG` environment variable:
```bash
$ export RUST_LOG=solana_runtime::native_loader=trace,solana_runtime::system_instruction_processor=trace,solana_runtime::bank=debug,solana_bpf_loader=debug,solana_rbpf=debug
```
To start a local Solana cluster run:
```bash
$ npm run localnet:update
$ npm run localnet:up
```
Solana cluster logs are available with:
```bash
$ npm run localnet:logs
```
For more details on working with a local cluster, see the [full
instructions](https://github.com/solana-labs/solana-web3.js#local-network).
### Build the on-chain program
```bash
$ npm run build:program
```
### Run the test client
```sh
$ npm run start
```
## Pointing to a public Solana cluster
Solana maintains three public clusters:
- `devnet` - Development cluster
- `testnet` - Tour De Sol test cluster
- `mainnet-beta` - Main cluster
Use npm scripts to configure which cluster.
To point to `devnet`:
```bash
$ npm run cluster:devnet
```
To point back to the local cluster:
```bash
$ npm run cluster:localnet
```

View File

@ -0,0 +1,20 @@
/**
* Exercises the token-lending program
*/
import { loadPrograms } from "./token-lending-test";
async function main() {
// These test cases are designed to run sequentially and in the following order
console.log("Run test: loadPrograms");
await loadPrograms();
console.log("Success\n");
}
main().then(
() => process.exit(),
(err) => {
console.error(err);
process.exit(-1);
}
);

View File

@ -0,0 +1,97 @@
import fs from "mz/fs";
import {
Account,
Connection,
BpfLoader,
PublicKey,
BPF_LOADER_PROGRAM_ID,
} from "@solana/web3.js";
import { Store } from "../client/util/store";
import { newAccountWithLamports } from "../client/util/new-account-with-lamports";
import { url } from "../client/util/url";
let connection: Connection | undefined;
async function getConnection(): Promise<Connection> {
if (connection) return connection;
connection = new Connection(url, "recent");
const version = await connection.getVersion();
console.log("Connection to cluster established:", url, version);
return connection;
}
export async function loadPrograms(): Promise<void> {
const connection = await getConnection();
const [tokenProgramId, tokenSwapProgramId] = await GetPrograms(connection);
console.log("Token Program ID", tokenProgramId.toString());
console.log("Token-swap Program ID", tokenSwapProgramId.toString());
}
async function loadProgram(
connection: Connection,
path: string
): Promise<PublicKey> {
const data = await fs.readFile(path);
const { feeCalculator } = await connection.getRecentBlockhash();
const loaderCost =
feeCalculator.lamportsPerSignature *
BpfLoader.getMinNumSignatures(data.length);
const minAccountBalance = await connection.getMinimumBalanceForRentExemption(
0
);
const minExecutableBalance = await connection.getMinimumBalanceForRentExemption(
data.length
);
const balanceNeeded = minAccountBalance + loaderCost + minExecutableBalance;
const from = await newAccountWithLamports(connection, balanceNeeded);
const program_account = new Account();
console.log("Loading program:", path);
await BpfLoader.load(
connection,
from,
program_account,
data,
BPF_LOADER_PROGRAM_ID
);
return program_account.publicKey;
}
async function GetPrograms(
connection: Connection
): Promise<[PublicKey, PublicKey]> {
const store = new Store();
let tokenProgramId = null;
let tokenLendingProgramId = null;
try {
const config = await store.load("config.json");
console.log("Using pre-loaded Token and Token-lending programs");
console.log(
" Note: To reload programs remove client/util/store/config.json"
);
if ("tokenProgramId" in config && "tokenLendingProgramId" in config) {
tokenProgramId = new PublicKey(config["tokenProgramId"]);
tokenLendingProgramId = new PublicKey(config["tokenLendingProgramId"]);
} else {
throw new Error("Program ids not found");
}
} catch (err) {
tokenProgramId = await loadProgram(
connection,
"../../target/bpfel-unknown-unknown/release/spl_token.so"
);
tokenLendingProgramId = await loadProgram(
connection,
"../../target/bpfel-unknown-unknown/release/spl_token_lending.so"
);
await store.save("config.json", {
tokenProgramId: tokenProgramId.toString(),
tokenSwapProgramId: tokenLendingProgramId.toString(),
});
}
return [tokenProgramId, tokenLendingProgramId];
}

View File

@ -0,0 +1,9 @@
import { Connection } from "@solana/web3.js";
export class TokenLending {
connection: Connection;
constructor(connection: Connection) {
this.connection = connection;
}
}

View File

@ -0,0 +1,14 @@
import { Account, Connection } from "@solana/web3.js";
export async function newAccountWithLamports(
connection: Connection,
lamports = 1000000
): Promise<Account> {
const account = new Account();
const signature = await connection.requestAirdrop(
account.publicKey,
lamports
);
await connection.confirmTransaction(signature);
return account;
}

View File

@ -0,0 +1,4 @@
// zzz
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@ -0,0 +1,27 @@
/**
* Simple file-based datastore
*/
import path from "path";
import fs from "mz/fs";
import mkdirp from "mkdirp";
type Config = { [key: string]: string };
export class Store {
static getDir(): string {
return path.join(__dirname, "store");
}
async load(uri: string): Promise<Config> {
const filename = path.join(Store.getDir(), uri);
const data = await fs.readFile(filename, "utf8");
return JSON.parse(data) as Config;
}
async save(uri: string, config: Config): Promise<void> {
await mkdirp(Store.getDir());
const filename = path.join(Store.getDir(), uri);
await fs.writeFile(filename, JSON.stringify(config), "utf8");
}
}

View File

@ -0,0 +1,37 @@
// To connect to a public cluster, set `export LIVE=1` in your
// environment. By default, `LIVE=1` will connect to the devnet cluster.
import { clusterApiUrl, Cluster } from "@solana/web3.js";
import dotenv from "dotenv";
function chooseCluster(): Cluster | undefined {
dotenv.config();
if (!process.env.LIVE) return;
switch (process.env.CLUSTER) {
case "devnet":
case "testnet":
case "mainnet-beta": {
return process.env.CLUSTER;
}
}
if (process.env.CLUSTER) {
throw new Error(
`Unknown cluster "${process.env.CLUSTER}", check the .env file`
);
} else {
throw new Error("CLUSTER is not specified, check the .env file");
}
}
export const cluster = chooseCluster();
export const url =
process.env.RPC_URL ||
(process.env.LIVE ? clusterApiUrl(cluster, false) : "http://localhost:8899");
export const urlTls =
process.env.RPC_URL ||
(process.env.LIVE ? clusterApiUrl(cluster, true) : "http://localhost:8899");
export const walletUrl =
process.env.WALLET_URL || "https://solana-example-webwallet.herokuapp.com/";

View File

@ -0,0 +1,2 @@
LIVE=1
CLUSTER=devnet

View File

@ -0,0 +1,2 @@
LIVE=1
CLUSTER=mainnet-beta

View File

@ -0,0 +1,2 @@
LIVE=1
CLUSTER=testnet

2722
token-lending/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
{
"name": "@solana/spl-token-lending",
"version": "0.1.0",
"description": "SPL Token Lending JavaScript API",
"license": "MIT",
"author": "Solana Maintainers <maintainers@solana.foundation>",
"repository": {
"type": "git",
"url": "https://github.com/solana-labs/solana-program-library"
},
"bugs": {
"url": "https://github.com/solana-labs/solana-program-library/issues"
},
"keywords": [],
"publishConfig": {
"access": "public"
},
"main": "lib/index.cjs.js",
"module": "lib/index.esm.js",
"types": "lib/index.d.ts",
"files": [
"lib"
],
"testnetDefaultChannel": "edge",
"scripts": {
"build": "rollup -c rollup.config.ts",
"build:program": "rm client/util/store/config.json; ../../do.sh build token-lending",
"start": "ts-node cli/main.ts",
"lint": "eslint --ext .ts {cli,client}/* && prettier --check \"{cli,client}/**/*.ts\"",
"lint:fix": "eslint --ext .ts {cli,client}/* --fix && prettier --write \"{cli,client}/**/*.ts\"",
"cluster:localnet": "rm -f .env",
"cluster:devnet": "cp cluster-devnet.env .env",
"cluster:testnet": "cp cluster-testnet.env .env",
"cluster:mainnet-beta": "cp cluster-mainnet-beta.env .env",
"localnet:update": "solana-localnet update",
"localnet:up": "rm client/util/store/config.json; set -x; solana-localnet down; set -e; solana-localnet up",
"localnet:down": "solana-localnet down",
"localnet:logs": "solana-localnet logs -f"
},
"dependencies": {
"@solana/web3.js": "^0.78.4"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^15.1.0",
"@tsconfig/recommended": "^1.0.1",
"@types/eslint": "^7.2.3",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/mkdirp": "^1.0.1",
"@types/mz": "^2.7.1",
"@types/node": "^14.11.5",
"@types/prettier": "^2.1.1",
"@types/rollup-plugin-json": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"dotenv": "^8.2.0",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-prettier": "^3.1.4",
"prettier": "^2.1.2",
"rollup": "^2.28.2",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-sourcemaps": "^0.6.2",
"rollup-plugin-typescript2": "^0.27.3",
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
},
"engines": {
"node": ">= 10"
}
}

View File

@ -0,0 +1,26 @@
import commonjs from '@rollup/plugin-commonjs'
import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
import sourceMaps from 'rollup-plugin-sourcemaps'
import typescript from 'rollup-plugin-typescript2'
const pkg = require('./package.json')
export default {
input: `client/index.ts`,
output: [
{ file: pkg.main, format: 'cjs', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true }
],
external: [],
watch: {
include: 'src/**'
},
plugins: [
json(),
typescript({useTsconfigDeclarationDir: false}),
commonjs(),
resolve(),
sourceMaps()
]
}

View File

@ -0,0 +1,15 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
},
"compilerOptions": {
"declaration": true,
"moduleResolution": "node",
"module": "es2015"
},
"include": ["cli/**/*", "client/**/*"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,30 @@
# Note: This crate must be built using do.sh
[package]
name = "spl-token-lending"
version = "0.1.0"
description = "Solana Program Library Token Lending"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2018"
[features]
no-entrypoint = []
program = ["solana-sdk/program"]
default = ["solana-sdk/default"]
[dependencies]
arrayref = "0.3.6"
num_enum = "0.5.1"
num-derive = "0.3"
num-traits = "0.2"
solana-sdk = { version = "1.3.14", default-features = false }
thiserror = "1.0"
[dev-dependencies]
rand = { version = "0.7.0"}
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,15 @@
language = "C"
header = "/* Autogenerated SPL Token-Lending program C Bindings */"
pragma_once = true
cpp_compat = true
line_length = 80
tab_width = 4
style = "both"
[export]
prefix = "TokenLending_"
include = ["LendingInstruction", "State"]
[parse]
parse_deps = true
include = ["solana-sdk"]

View File

@ -0,0 +1,24 @@
//! Program entrypoint definitions
#![cfg(feature = "program")]
#![cfg(not(feature = "no-entrypoint"))]
use crate::{error::LendingError, processor::Processor};
use solana_sdk::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
program_error::PrintProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
fn process_instruction<'a>(
program_id: &Pubkey,
accounts: &'a [AccountInfo<'a>],
instruction_data: &[u8],
) -> ProgramResult {
if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<LendingError>();
return Err(error);
}
Ok(())
}

View File

@ -0,0 +1,25 @@
//! Error types
use num_derive::FromPrimitive;
use solana_sdk::{decode_error::DecodeError, program_error::ProgramError};
use thiserror::Error;
/// Errors that may be returned by the TokenLending program.
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum LendingError {
/// The account cannot be initialized because it is already being used.
#[error("Lending account already in use")]
AlreadyInUse,
}
impl From<LendingError> for ProgramError {
fn from(e: LendingError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for LendingError {
fn type_of() -> &'static str {
"Lending Error"
}
}

View File

@ -0,0 +1,15 @@
//! Instruction types
/// Instructions supported by the lending program.
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub enum LendingInstruction {
/// Initializes a new lending pool.
InitPool,
// InitReserve,
// Deposit,
// Withdraw,
// Borrow,
// Repay,
// Liquidate,
}

View File

@ -0,0 +1,15 @@
#![deny(missing_docs)]
//! A lending program for the Solana blockchain.
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
// Export current solana-sdk types for downstream users who may also be building with a different
// solana-sdk version
pub use solana_sdk;
solana_sdk::declare_id!("TokenLend1ng1111111111111111111111111111111");

View File

@ -0,0 +1,35 @@
//! Program state processor
#![cfg(feature = "program")]
use crate::error::LendingError;
use num_traits::FromPrimitive;
use solana_sdk::{
account_info::AccountInfo, decode_error::DecodeError, entrypoint::ProgramResult, info,
program_error::PrintProgramError, pubkey::Pubkey,
};
/// Program state handler.
pub struct Processor {}
impl Processor {
/// Processes an instruction
pub fn process(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_input: &[u8],
) -> ProgramResult {
Ok(())
}
}
impl PrintProgramError for LendingError {
fn print<E>(&self)
where
E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
{
match self {
LendingError::AlreadyInUse => info!("Error: Lending account already in use"),
}
}
}

View File

@ -0,0 +1,16 @@
//! State types
/// Lending pool state
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PoolState {}
/// Pool reserve state
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ReserveState {}
/// Borrow obligation state
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ObligationState {}