Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/app/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Sunrise Stake App

(pre-trees version)

The UI for [Sunrise Stake](https://app.sunrisestake.com)

## Getting started
Expand Down
1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@solana/web3.js": "^1.66.2",
"@sunrisestake/client": "*",
"@sunrisestake/marinade-ts-sdk": "4.0.4-alpha.18",
"@sunrisestake/yield-controller": "^0.0.4",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.7",
"@testing-library/jest-dom": "^5.16.5",
Expand Down
51 changes: 31 additions & 20 deletions packages/app/src/hooks/useCarbon.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
import { useSunriseStake } from "../context/sunriseStakeContext";
import { useEffect, useState } from "react";
import BN from "bn.js";
import { solToCarbon, toSol } from "../lib/util";
import { useConnection } from "@solana/wallet-adapter-react";
import { useEffect, useState } from "react";
import { PRICES, solToCarbon, toSol } from "../lib/util";
import { useSunriseStake } from "../context/sunriseStakeContext";
import { useYieldController } from "./useYieldController";

export const useCarbon = (): { totalCarbon: number | undefined } => {
const { connection } = useConnection();
const { details, client } = useSunriseStake();
const useCarbon = (): { totalCarbon: number | undefined } => {
const { details } = useSunriseStake();
const yieldControllerState = useYieldController();
const [totalCarbon, setTotalCarbon] = useState<number>();

useEffect(() => {
void (async () => {
if (details == null || client == null) return;
if (details == null || yieldControllerState == null) return;
// TODO extract to some library
// Total carbon is the carbon value of
// 1. the extractable yield
// 2. the treasury balance
// 3. the holding account balance
// (TODO this last one will be replaced with the TreasuryController total_spent value)
// 3. the YieldController totalTokensPurchased value

const extractableYield = details.extractableYield;
const treasuryBalance = new BN(details.balances.treasuryBalance);
const holdingAccountBalance = new BN(
await connection.getBalance(client.holdingAccount)
);
// no longer used to calculate the amount - can be removed
// const holdingAccountBalance = new BN(
// details.balances.holdingAccountBalance
// );
// this is the amount of carbon tokens burned so far by the protocol
const totalTokensPurchased =
(yieldControllerState?.totalTokensPurchased ?? new BN(0)).toNumber() /
10 ** 8; // tokens purchased are stored on-chain in minor denomination

// this is the current price set in the yield controller for buying carbon tokens
// TODO use this instead of the hard-coded values to convert SOL to Carbon
// const price = yieldControllerState?.price ?? 0;

const totalLamports = extractableYield
.add(treasuryBalance)
.add(holdingAccountBalance);
const totalLamportsWaiting = extractableYield.add(treasuryBalance);

const carbon = solToCarbon(toSol(totalLamports));
const carbon =
solToCarbon(toSol(totalLamportsWaiting)) + totalTokensPurchased;

console.log({
extractableYield: toSol(extractableYield),
treasuryBalance: toSol(treasuryBalance),
holdingAccountBalance: toSol(holdingAccountBalance),
totalLamports: toSol(totalLamports),
// holdingAccountBalance: toSol(holdingAccountBalance),
totalLamportsWaiting: toSol(totalLamportsWaiting),
totalTokensPurchased,
totalCarbon: carbon,
prices: PRICES,
});

// due to fees, at low values, the total can be negative
const normalizedCarbon = carbon < 0 ? 0 : carbon;

setTotalCarbon(normalizedCarbon);
})();
}, [details]);
}, [details, yieldControllerState]);

return { totalCarbon };
};

export { useCarbon };
50 changes: 50 additions & 0 deletions packages/app/src/hooks/useYieldController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import {
type YieldControllerState,
YieldControllerClient,
} from "@sunrisestake/yield-controller";
import { AnchorProvider } from "@coral-xyz/anchor";
import { useConnection } from "@solana/wallet-adapter-react";
import { Environment } from "@sunrisestake/client";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { Keypair } from "@solana/web3.js";

const stage =
(process.env.REACT_APP_SOLANA_NETWORK as keyof typeof Environment) ??
WalletAdapterNetwork.Devnet;

export const useYieldController = (): YieldControllerState | undefined => {
const { connection } = useConnection();
const [yieldState, setYieldState] = useState<YieldControllerState>();
useEffect(() => {
void (async () => {
const provider = new AnchorProvider(
connection,
// we only need a read-only wallet - this allows us to get the yield status
// before the user has connected
{
publicKey: Keypair.generate().publicKey,
signAllTransactions: async (txes) => txes,
signTransaction: async (tx) => tx,
},
{}
);
const env = Environment[stage];
const yieldControllerClient = await YieldControllerClient.get(
provider,
env.yieldControllerState
).catch((e) => {
console.error(e);
throw e;
});
yieldControllerClient
.getState()
.then(setYieldState)
.catch((e) => {
console.error(e);
});
})();
}, [connection]);

return yieldState;
};
6 changes: 5 additions & 1 deletion packages/app/src/lib/sunriseClientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const stage =
export class SunriseClientWrapper {
public debouncedUpdate = debounce(this.triggerUpdate.bind(this), 1000);
constructor(
private readonly client: SunriseStakeClient,
readonly client: SunriseStakeClient,
private readonly detailsListener: ((details: Details) => void) | undefined,
readonly readonlyWallet: boolean
) {
Expand Down Expand Up @@ -140,6 +140,10 @@ export class SunriseClientWrapper {
return this.client.env.holdingAccount;
}

get yieldControllerState(): PublicKey {
return this.client.env.yieldControllerState;
}

async lockGSol(amount: BN): Promise<string> {
if (this.readonlyWallet) throw new Error("Readonly wallet");
return this.client
Expand Down
51 changes: 39 additions & 12 deletions packages/app/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { LAMPORTS_PER_SOL, type PublicKey } from "@solana/web3.js";
import { type SignerWalletAdapterProps } from "@solana/wallet-adapter-base";
import BN from "bn.js";

export const ZERO = new BN(0);
const ZERO = new BN(0);

export const toBN = (n: number): BN => new BN(`${n}`);
const toBN = (n: number): BN => new BN(`${n}`);

export const walletIsConnected = (
const walletIsConnected = (
wallet: SparseWalletContextAdapter
): wallet is ConnectedWallet => wallet.connected && wallet.publicKey != null;

Expand All @@ -18,7 +18,7 @@ export const toSol = (lamports: BN, precision = MAX_NUM_PRECISION): number =>
lamports.div(new BN(10).pow(new BN(precision))).toNumber() /
(LAMPORTS_PER_SOL / 10 ** precision);

export const solToLamports = (sol: number | string): BN => {
const solToLamports = (sol: number | string): BN => {
// handle very big numbers but also integers.
// note this doesn't handle large numbers with decimals.
// in other words, if you ask for eg a withdrawal of 1e20 SOL + 0.1 SOL, it will round that to 1e20 SOL.TODO fix this later.
Expand All @@ -41,12 +41,12 @@ const formatPrecision = (n: number, precision = MAX_NUM_PRECISION): number =>
precision
);

export const toFixedWithPrecision = (
const toFixedWithPrecision = (
n: number,
precision = MAX_NUM_PRECISION
): string => n.toFixed(formatPrecision(n, precision));

export const getDigits = (strNo: string): number | undefined => {
const getDigits = (strNo: string): number | undefined => {
const match = strNo.match(/^\d*\.(\d+)$/);
if (match?.[1] != null) return match[1].length;
};
Expand All @@ -64,14 +64,30 @@ type SparseWalletContextAdapter = Omit<SparseWallet, "publicKey"> & {

export type ConnectedWallet = SparseWallet & { connected: true };

// TODO TEMP lookup
export const SOL_PRICE_USD_CENTS = 2500;
export const CARBON_PRICE_USD_CENTS_PER_TONNE = 173; // NCT price in USD cents
const DEFAULT_SOLANA_USD_PRICE = 2000; // SOL price in USD cents
const DEFAULT_NCT_USD_PRICE = 200; // NCT price in USD cents

export const solToCarbon = (sol: number): number =>
(sol * SOL_PRICE_USD_CENTS) / CARBON_PRICE_USD_CENTS_PER_TONNE;
interface Prices {
solana: number;
nct: number;
}
export const PRICES: Prices = {
solana: DEFAULT_SOLANA_USD_PRICE,
nct: DEFAULT_NCT_USD_PRICE,
};

fetch("https://api.sunrisestake.com/prices")
.then(async (res) => res.json())
.then(({ solana, "toucan-protocol-nature-carbon-tonne": nct }) => {
console.log("Prices", { solana, nct });
PRICES.solana = Number(solana.usd) * 100;
PRICES.nct = Number(nct.usd) * 100;
})
.catch(console.error);

export function debounce<F extends (...args: Parameters<F>) => ReturnType<F>>(
const solToCarbon = (sol: number): number => (sol * PRICES.solana) / PRICES.nct;

function debounce<F extends (...args: Parameters<F>) => ReturnType<F>>(
func: F,
waitFor: number
): (...args: Parameters<F>) => void {
Expand All @@ -81,3 +97,14 @@ export function debounce<F extends (...args: Parameters<F>) => ReturnType<F>>(
timeout = setTimeout(() => func(...args), waitFor);
};
}

export {
ZERO,
debounce,
getDigits,
solToCarbon,
solToLamports,
toBN,
toFixedWithPrecision,
walletIsConnected,
};
2 changes: 1 addition & 1 deletion packages/app/src/utils/tooltips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const tooltips = {
totalStake: <>The sum of everyones staked SOL</>,
offsetCO2: (
<>
Tonnes of Carbon Dioxide or equivalent (tCO2e) offset by Sunrise so far.
Tonnes of Carbon Dioxide or equivalent (tCO₂E) offset by Sunrise so far.
Note: This number includes yield that Sunrise has accrued, but not yet
spent
</>
Expand Down
16 changes: 16 additions & 0 deletions packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { PublicKey } from "@solana/web3.js";
import { type EpochReportAccount } from "./types/EpochReportAccount";
import BN from "bn.js";

// on devnet, the solblaze pool is sometimes unavailable for deposits - use this to disable it
export const SOLBLAZE_ENABLED = false;

export const STAKE_POOL_PROGRAM_ID = new PublicKey(
"SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy"
);
Expand All @@ -15,6 +18,7 @@ interface BlazeConfig {
export interface EnvironmentConfig {
state: PublicKey;
holdingAccount: PublicKey;
yieldControllerState: PublicKey;
percentageStakeToMarinade: number;
blaze: BlazeConfig;
}
Expand All @@ -27,6 +31,9 @@ export const Environment: Record<
holdingAccount: new PublicKey(
"shcFT8Ur2mzpX61uWQRL9KyERZp4w2ehDEvA7iaAthn"
),
yieldControllerState: new PublicKey(
"htGs6L3pCRxgfkJP2vLUdb9hVPtcE4mKsdWP4CnirQA"
),
percentageStakeToMarinade: 200, // TODO TEMP fix
blaze: {
pool: new PublicKey("stk9ApL5HeVAwPLr3TLhDXdZS8ptVu7zp6ov8HFDuMi"),
Expand All @@ -38,6 +45,9 @@ export const Environment: Record<
state: new PublicKey("DR3hrjH6SZefraRu8vaQfEhG5e6E25ZwccakQxWRePkC"), // Warning obsolete
holdingAccount: PublicKey.default,
percentageStakeToMarinade: 75,
yieldControllerState: new PublicKey(
"77aJfgRudbv9gFfjRQw3tuYzgnjoDgs9jorVTmK7cv73"
),
blaze: {
pool: PublicKey.default,
bsolMint: PublicKey.default,
Expand All @@ -48,6 +58,9 @@ export const Environment: Record<
holdingAccount: new PublicKey(
"dhcB568T3skiP2D9ujf4eAJEnW2gACaaA9BUCVbwbXD"
),
yieldControllerState: new PublicKey(
"77aJfgRudbv9gFfjRQw3tuYzgnjoDgs9jorVTmK7cv73"
),
percentageStakeToMarinade: 75,
blaze: {
pool: new PublicKey("azFVdHtAJN8BX3sbGAYkXvtdjdrT5U6rj9rovvUFos9"),
Expand All @@ -59,6 +72,9 @@ export const Environment: Record<
holdingAccount: new PublicKey(
"dhcB568T3skiP2D9ujf4eAJEnW2gACaaA9BUCVbwbXD"
),
yieldControllerState: new PublicKey(
"77aJfgRudbv9gFfjRQw3tuYzgnjoDgs9jorVTmK7cv73"
),
percentageStakeToMarinade: 75,
blaze: {
pool: new PublicKey("azFVdHtAJN8BX3sbGAYkXvtdjdrT5U6rj9rovvUFos9"),
Expand Down
29 changes: 22 additions & 7 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
type EnvironmentConfig,
MARINADE_TICKET_RENT,
NETWORK_FEE,
SOLBLAZE_ENABLED,
STAKE_POOL_PROGRAM_ID,
} from "./constants";
import {
Expand Down Expand Up @@ -240,7 +241,10 @@ export class SunriseStakeClient {

public async makeBalancedDeposit(lamports: BN): Promise<Transaction> {
const details = await this.details();
if (marinadeTargetReached(details, this.env.percentageStakeToMarinade)) {
if (
marinadeTargetReached(details, this.env.percentageStakeToMarinade) &&
SOLBLAZE_ENABLED
) {
console.log("Routing deposit to Solblaze");
return this.depositToBlaze(lamports);
}
Expand Down Expand Up @@ -853,9 +857,9 @@ export class SunriseStakeClient {
): WithdrawalFees {
// Calculate how much can be withdrawn from the lp (without fee)
const lpSolShare = details.lpDetails.lpSolShare;
const preferredMinLiqPoolValue = new BN(
details.balances.gsolSupply.amount
).muln(0.1);
const preferredMinLiqPoolValue = new BN(details.balances.gsolSupply.amount)
.muln(DEFAULT_LP_MIN_PROPORTION)
.divn(100);
const postUnstakeLpSolValue = new BN(lpSolShare).sub(withdrawalLamports);

// Calculate how much will be withdrawn through liquid unstaking (with fee)
Expand All @@ -870,12 +874,23 @@ export class SunriseStakeClient {
? MARINADE_TICKET_RENT
: 0;

this.log("amount to order unstake: ", amountToOrderUnstake);
this.log("rent for order unstake: ", rentForOrderUnstakeTicket);
this.log("withdrawal lamports: ", withdrawalLamports.toString());
this.log("lp sol share: ", lpSolShare.toString());
this.log("preferred min lp value: ", preferredMinLiqPoolValue.toString());
this.log("post unstake lp sol value: ", postUnstakeLpSolValue.toString());
this.log(
"amount being liquid unstaked: ",
amountBeingLiquidUnstaked.toString()
);
this.log("amount to order unstake: ", amountToOrderUnstake.toString());
this.log("rent for order unstake: ", rentForOrderUnstakeTicket.toString());

const ticketFee = rentForOrderUnstakeTicket;

let totalFee = new BN(rentForOrderUnstakeTicket + 2 * NETWORK_FEE);
let totalFee =
rentForOrderUnstakeTicket > 0
? new BN(rentForOrderUnstakeTicket + 2 * NETWORK_FEE)
: ZERO;

if (amountBeingLiquidUnstaked.lte(ZERO)) {
return {
Expand Down
4 changes: 2 additions & 2 deletions packages/scripts/extract.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SunriseStakeClient } from "../app/src/lib/client/";
import { SunriseStakeClient } from "../client/src";
import "./util";
import { AnchorProvider } from "@project-serum/anchor";
import { SUNRISE_STAKE_STATE } from "@sunrisestake/app/src/lib/constants";
import { SUNRISE_STAKE_STATE } from "../client/src/constants";

(async () => {
const provider = AnchorProvider.env();
Expand Down
Loading