Skip to content

Commit

Permalink
SPL token lending scaffolding (solana-labs#582)
Browse files Browse the repository at this point in the history
* Scaffolding for spl-token-lending program

* Scaffolding for TS client
  • Loading branch information
jstarry authored Oct 9, 2020
1 parent 5bd2e1c commit a4f09fe
Show file tree
Hide file tree
Showing 32 changed files with 3,391 additions and 2 deletions.
13 changes: 11 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -22,7 +31,7 @@ updates:
directory: "/"
schedule:
interval: daily
time: "01:00"
time: "04:00"
timezone: America/Los_Angeles
labels:
- "automerge"
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"memo/program",
"themis/program_bn",
"themis/program_ristretto",
"token-lending/program",
"token-swap/program",
"token/cli",
"token/program",
Expand Down
16 changes: 16 additions & 0 deletions ci/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ if [[ -z $1 ]]; then
programs=(
memo/program
token/program
token-lending/program
token-swap/program
)
else
Expand Down
7 changes: 7 additions & 0 deletions token-lending/README.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions token-lending/js/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions token-lending/js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
coverage
.nyc_output
.DS_Store
*.log
.vscode
.idea
dist
compiled
.awcache
.rpt2_cache
docs
lib
69 changes: 69 additions & 0 deletions token-lending/js/README.md
Original file line number Diff line number Diff line change
@@ -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
```
20 changes: 20 additions & 0 deletions token-lending/js/cli/main.ts
Original file line number Diff line number Diff line change
@@ -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);
}
);
97 changes: 97 additions & 0 deletions token-lending/js/cli/token-lending-test.ts
Original file line number Diff line number Diff line change
@@ -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];
}
9 changes: 9 additions & 0 deletions token-lending/js/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Connection } from "@solana/web3.js";

export class TokenLending {
connection: Connection;

constructor(connection: Connection) {
this.connection = connection;
}
}
14 changes: 14 additions & 0 deletions token-lending/js/client/util/new-account-with-lamports.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions token-lending/js/client/util/sleep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// zzz
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
27 changes: 27 additions & 0 deletions token-lending/js/client/util/store.ts
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading

0 comments on commit a4f09fe

Please sign in to comment.