Skip to content

Commit

Permalink
feat: Add trustline creation to the borrow flow.
Browse files Browse the repository at this point in the history
  • Loading branch information
kovipu committed Oct 27, 2024
1 parent 7309f77 commit 27e6e9b
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 27 deletions.
5 changes: 5 additions & 0 deletions currencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Currency = {
ticker: SupportedCurrency;
tokenContractAddress: string;
loanPoolName: string;
issuer?: string;
};

// The addresses here are for testnet.
Expand All @@ -25,27 +26,31 @@ export const CURRENCY_WBTC: Currency = {
ticker: 'wBTC',
tokenContractAddress: 'CAP5AMC2OHNVREO66DFIN6DHJMPOBAJ2KCDDIMFBR7WWJH5RZBFM3UEI',
loanPoolName: 'pool_wbtc',
issuer: 'GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56',
} as const;

export const CURRENCY_WETH: Currency = {
name: 'Ethereum',
ticker: 'wETH',
tokenContractAddress: 'CAZAQB3D7KSLSNOSQKYD2V4JP5V2Y3B4RDJZRLBFCCIXDCTE3WHSY3UE',
loanPoolName: 'pool_weth',
issuer: 'GATALTGTWIOT6BUDBCZM3Q4OQ4BO2COLOAZ7IYSKPLC2PMSOPPGF5V56',
} as const;

export const CURRENCY_USDC: Currency = {
name: 'USD Coin',
ticker: 'USDC',
tokenContractAddress: 'CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA',
loanPoolName: 'pool_usdc',
issuer: 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5',
} as const;

export const CURRENCY_EURC: Currency = {
name: 'Euro Coin',
ticker: 'EURC',
tokenContractAddress: 'CCUUDM434BMZMYWYDITHFXHDMIVTGGD6T2I5UKNX5BSLXLW7HVR4MCGZ',
loanPoolName: 'pool_eurc',
issuer: 'GB3Q6QDZYTHWT7E5PVS3W7FUT5GVAFC5KSZFFLPU25GO7VTC3NM2ZTVO',
} as const;

export const CURRENCIES: Currency[] = [
Expand Down
2 changes: 1 addition & 1 deletion src/components/CryptoAmountSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const CryptoAmountSelector = ({
const TickerOption = ({ ticker }: { ticker: SupportedCurrency }) => {
const { walletBalances } = useWallet();
const balance = walletBalances?.[ticker];
const disabled = isNil(balance) || !balance.trustline || isBalanceZero(balance.balanceLine.balance);
const disabled = isNil(balance) || !balance.trustLine || isBalanceZero(balance.balanceLine.balance);

return (
<option disabled={disabled} value={ticker}>
Expand Down
34 changes: 34 additions & 0 deletions src/lib/horizon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Asset, Horizon, Networks, Operation, type Transaction, TransactionBuilder } from '@stellar/stellar-sdk';
import type { XDR_BASE64 } from '@stellar/stellar-sdk/contract';
import type { Currency } from 'currencies';

const HorizonServer = new Horizon.Server('https://horizon-testnet.stellar.org/');

export const getBalances = async (account: string): Promise<Horizon.HorizonApi.BalanceLine[]> => {
const { balances } = await HorizonServer.loadAccount(account);
return balances;
};

export const createAddTrustlineTransaction = async (
account: string,
{ ticker, issuer }: Currency,
): Promise<Transaction> => {
const asset = new Asset(ticker, issuer);

const sourceAccount = await HorizonServer.loadAccount(account);

const transaction = new TransactionBuilder(sourceAccount, {
networkPassphrase: Networks.TESTNET,
fee: '100000',
})
.addOperation(Operation.changeTrust({ asset }))
.setTimeout(300) // 5 minutes timeout
.build();

return transaction;
};

export const sendTransaction = async (txXdr: XDR_BASE64): Promise<Horizon.HorizonApi.SubmitTransactionResponse> => {
const tx = TransactionBuilder.fromXDR(txXdr, Networks.TESTNET);
return HorizonServer.submitTransaction(tx);
};
19 changes: 12 additions & 7 deletions src/pages/_borrow/BorrowModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as StellarSdk from '@stellar/stellar-sdk'
import { Warning } from '@components/Alert';
import { Button } from '@components/Button';
import { CryptoAmountSelector } from '@components/CryptoAmountSelector';
import { Loading } from '@components/Loading';
import { contractClient as loanManagerClient } from '@contracts/loan_manager';
import { getIntegerPart, to7decimals } from '@lib/converters';
import { SCALAR_7, fromCents, toCents } from '@lib/formatting';
import { createAddTrustlineTransaction, sendTransaction } from '@lib/horizon';
import type { SupportedCurrency } from 'currencies';
import { type ChangeEvent, useState } from 'react';
import { CURRENCY_BINDINGS, CURRENCY_BINDINGS_ARR, type CurrencyBinding } from 'src/currency-bindings';
Expand Down Expand Up @@ -37,8 +37,8 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
({ ticker }) => ticker,
);

// The modal is impossible to open before the balances load.
if (!walletBalances) return null;
// The modal is impossible to open without a wallet or before the balances load.
if (!wallet || !walletBalances) return null;

const loanBalance = walletBalances[ticker];
const collateralBalance = walletBalances[collateralTicker];
Expand All @@ -55,7 +55,7 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
loanAmountCents && loanAmountCents > 0n ? Number(collateralAmountCents) / Number(loanAmountCents) : 0;

// The modal is impossible to open without collateral balance.
if (!collateralBalance.trustline) return null;
if (!collateralBalance.trustLine) return null;

const closeModal = () => {
refetchBalances();
Expand All @@ -64,6 +64,12 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
onClose();
};

const handleAddTrustlineClick = async () => {
const tx = await createAddTrustlineTransaction(wallet.address, currency);
const signedTx = await signTransaction(tx.toXDR());
sendTransaction(signedTx);
};

const handleBorrowClick = async () => {
if (!wallet) {
alert('Please connect your wallet first!');
Expand All @@ -81,8 +87,6 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro

const { contractId: collateralCurrencyId } = CURRENCY_BINDINGS[collateralTicker];



const tx = await loanManagerClient.create_loan({
user: wallet.address,
borrowed: to7decimals(loanAmount),
Expand Down Expand Up @@ -126,7 +130,7 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
setCollateralAmount('0');
};

const isTrustline = loanBalance.trustline;
const isTrustline = loanBalance.trustLine;

const isBorrowDisabled =
!isTrustline || loanAmount === '0' || collateralAmount === '0' || healthFactor < HEALTH_FACTOR_MIN_THRESHOLD;
Expand All @@ -149,6 +153,7 @@ export const BorrowModal = ({ modalId, onClose, currency, totalSupplied }: Borro
{!isTrustline ? (
<Warning>
<span>Create a trustline for {ticker} in your wallet first!</span>
<Button onClick={handleAddTrustlineClick}>Add trustline</Button>
</Warning>
) : null}
<p className="my-4">
Expand Down
4 changes: 2 additions & 2 deletions src/pages/_borrow/BorrowableAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => {
? false
: Object.entries(walletBalances)
.filter(([t, _b]) => t !== ticker)
.some(([_t, b]) => b.trustline && !isBalanceZero(b.balanceLine.balance));
.some(([_t, b]) => b.trustLine && !isBalanceZero(b.balanceLine.balance));

const borrowDisabled = !wallet || !isCollateral || !totalSupplied;

Expand Down Expand Up @@ -66,7 +66,7 @@ export const BorrowableAsset = ({ currency }: BorrowableAssetCardProps) => {
const { result } = await loanManagerClient.get_price({ token: currency.ticker });
setTotalSuppliedPrice(result);
} catch (error) {
console.error('Error fetchin price data:', error);
console.error('Error fetching price data:', error);
}
}, [currency.ticker]);

Expand Down
33 changes: 16 additions & 17 deletions src/stellar-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { FREIGHTER_ID, StellarWalletsKit, WalletNetwork, allowAllModules } from '@creit.tech/stellar-wallets-kit';
import * as StellarSdk from '@stellar/stellar-sdk';
import type * as StellarSdk from '@stellar/stellar-sdk';
import { type PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';

import { contractClient as loanManagerClient } from '@contracts/loan_manager';
import { getBalances } from '@lib/horizon';
import { type SupportedCurrency, isSupportedCurrency } from 'currencies';
import { CURRENCY_BINDINGS_ARR } from './currency-bindings';

const HorizonServer = new StellarSdk.Horizon.Server('https://horizon-testnet.stellar.org/');

export type Wallet = {
address: string;
displayName: string;
};

export type Balance =
| { trustline: false }
| { trustline: true; balanceLine: StellarSdk.Horizon.HorizonApi.BalanceLine };
| { trustLine: false }
| { trustLine: true; balanceLine: StellarSdk.Horizon.HorizonApi.BalanceLine };

export type BalanceRecord = {
[K in SupportedCurrency]: Balance;
Expand Down Expand Up @@ -61,8 +60,8 @@ const Context = createContext<WalletContext>({
walletBalances: null,
positions: {},
prices: null,
createConnectWalletButton: () => { },
refetchBalances: () => { },
createConnectWalletButton: () => {},
refetchBalances: () => {},
signTransaction: () => Promise.reject(),
});

Expand Down Expand Up @@ -91,18 +90,18 @@ const createBalanceRecord = (balances: StellarSdk.Horizon.HorizonApi.BalanceLine
balances.reduce(
(acc, balanceLine) => {
if (balanceLine.asset_type === 'native') {
acc.XLM = { trustline: true, balanceLine };
acc.XLM = { trustLine: true, balanceLine };
} else if (balanceLine.asset_type === 'credit_alphanum4' && isSupportedCurrency(balanceLine.asset_code)) {
acc[balanceLine.asset_code] = { trustline: true, balanceLine };
acc[balanceLine.asset_code] = { trustLine: true, balanceLine };
}
return acc;
},
{
XLM: { trustline: false },
wBTC: { trustline: false },
wETH: { trustline: false },
USDC: { trustline: false },
EURC: { trustline: false },
XLM: { trustLine: false },
wBTC: { trustLine: false },
wETH: { trustLine: false },
USDC: { trustLine: false },
EURC: { trustLine: false },
} as BalanceRecord,
);

Expand All @@ -122,7 +121,7 @@ const fetchPriceData = async (token: string): Promise<bigint> => {
const { result } = await loanManagerClient.get_price({ token });
return result;
} catch (error) {
console.error('Error fetching price data:', error);
console.error(`Error fetching price data: for ${token}`, error);
return 0n;
}
};
Expand All @@ -135,7 +134,7 @@ export const WalletProvider = ({ children }: PropsWithChildren) => {

const setWallet = async (address: string) => {
setAddress(address);
const { balances } = await HorizonServer.loadAccount(address);
const balances = await getBalances(address);
setWalletBalances(createBalanceRecord(balances));
setPositions(await fetchAllPositions(address));
};
Expand Down Expand Up @@ -172,7 +171,7 @@ export const WalletProvider = ({ children }: PropsWithChildren) => {
if (!address) return;

try {
const { balances } = await HorizonServer.loadAccount(address);
const balances = await getBalances(address);
setWalletBalances(createBalanceRecord(balances));
const positions = await fetchAllPositions(address);
setPositions(positions);
Expand Down

0 comments on commit 27e6e9b

Please sign in to comment.