juna
Version:
A cross platform NFT lending client for serious lenders
224 lines (199 loc) • 7.26 kB
text/typescript
import puppeteer from "puppeteer-extra";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import { Page, Browser } from "puppeteer";
import { PrivateKeyAccount, createWalletClient, WalletClient, http } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { BlurLoan, BlurOffer } from "./support/types";
import { LendingPlatform, Offer, OfferType } from "../types";
import { BETH } from "../support/currencies";
import { config } from "../config";
puppeteer.use(StealthPlugin());
export class GhostApi {
private client: WalletClient;
private account: PrivateKeyAccount;
private browser: Browser | null;
private page: Page | null;
private initialised: boolean;
constructor(privateKey: `0x${string}`, rpcUrl: `https://${string}` = config.defaultRpc) {
this.account = privateKeyToAccount(privateKey);
this.client = createWalletClient({
account: this.account,
chain: mainnet,
transport: http(rpcUrl),
});
this.browser = null;
this.page = null;
this.initialised = false;
}
public async initialise() {
if (this.initialised) {
return;
}
this.browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-accelerated-2d-canvas",
"--no-first-run",
"--no-zygote",
"--disable-gpu",
],
});
this.page = await this.browser.newPage();
await this.page.goto(config.blur.baseUrlHome);
const challenge: any = await this.challenge();
const signedMessage = await this.account.signMessage({ message: challenge.message });
const authToken: any = await this.login(challenge, signedMessage);
this.createCookie(authToken);
this.initialised = true;
}
private getPage(): Page {
return this.page as Page;
}
private async get(url: string): Promise<any> {
return await this.getPage().evaluate(
async (url, options) => {
const response = await fetch(url, options);
return response.json();
},
url,
{
method: "GET",
credentials: "include",
} as RequestInit,
);
}
private async post(url: string, payload: {}): Promise<any> {
const response = (await this.getPage().evaluate(
async (url, options) => {
const response = await fetch(url, options);
return response.json();
},
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
credentials: "include",
} as RequestInit,
)) as any;
if (response.statusCode && response.statusCode !== 200) {
throw Error(response.message);
}
return response;
}
private async challenge() {
return await this.post(`${config.blur.baseUrlAuth}/challenge`, { walletAddress: this.account.address });
}
private async login(challenge: {}, signedMessage: `0x${string}`) {
return await this.post(`${config.blur.baseUrlAuth}/login`, { ...challenge, signature: signedMessage });
}
private async createCookie(authToken: { accessToken: string }) {
return await this.post(`${config.blur.baseUrlAuth}/cookie`, { authToken: authToken.accessToken });
}
public async getLiens(address: `0x${string}`): Promise<BlurLoan[]> {
await this.initialise();
const liens = await this.get(`${config.blur.baseUrlPortfolio}/${address.toLowerCase()}/liens`);
return liens.liens;
}
public async getLoanOffers(accountAddress: `0x${string}`, collectionAddress?: `0x${string}`): Promise<BlurOffer[]> {
await this.initialise();
const loans = await this.get(
`${config.blur.baseUrlPortfolio}/${accountAddress.toLowerCase()}/loan-offers?contractAddress=${collectionAddress}`,
);
return loans.loanOffers;
}
public async postLoanOffer(
collectionAddress: `0x${string}`,
principal: number,
limit: number,
apr: number,
expiryInMinutes: number,
): Promise<Offer> {
await this.initialise();
// Format the query
const format = await this.post(`${config.blur.baseUrlBlend}/loan-offer/format`, {
contractAddress: collectionAddress.toLowerCase(),
orders: [
{
contractAddress: collectionAddress.toLowerCase(),
expirationTime: new Date(new Date().getTime() + expiryInMinutes * 60 * 1000).toISOString(),
maxAmount: principal.toString(),
rate: apr * 10000,
totalAmount: limit.toString(),
},
],
userAddress: this.account.address.toLowerCase(),
});
// Signing
const signData = format.signatures[0].signData;
signData.value.minAmount = BigInt(signData.value.minAmount.hex);
signData.value.maxAmount = BigInt(signData.value.maxAmount.hex);
signData.value.totalAmount = BigInt(signData.value.totalAmount.hex);
signData.value.salt = BigInt(signData.value.salt.hex);
signData.value.auctionDuration = BigInt(signData.value.auctionDuration.hex);
signData.value.expirationTime = BigInt(signData.value.expirationTime.hex);
const signature = await this.account.signTypedData({
domain: signData.domain,
types: signData.types,
primaryType: "LoanOffer",
message: signData.value,
});
// Submit the offer
const response = await this.post(`${config.blur.baseUrlBlend}/loan-offer/submit`, {
contractAddress: collectionAddress.toLowerCase(),
orders: [
{
contractAddress: collectionAddress.toLowerCase(),
expirationTime: new Date(new Date().getTime() + expiryInMinutes * 60 * 1000).toISOString(),
maxAmount: principal.toString(),
rate: apr * 10000,
totalAmount: limit.toString(),
signature: signature,
marketplaceData: format.signatures[0].marketplaceData,
},
],
userAddress: this.account.address.toLowerCase(),
});
return {
id: response.hashes[0],
platform: LendingPlatform.blur,
lender: this.account.address,
offerDate: new Date(),
expiryDate: new Date(new Date().getTime() + expiryInMinutes * 60 * 1000),
type: OfferType.collectionOffer,
currency: BETH,
principal: principal,
durationInDays: 0,
apr: apr,
collateral: {
collectionAddress: collectionAddress,
collectionName: "",
nftId: "",
},
};
}
public async recallLoan(collectionAddress: `0x${string}`, loanId: string, nftId: string) {
await this.initialise();
// Fetching the tx data
const payload = {
userAddress: this.account.address.toLowerCase(),
contractAddress: collectionAddress.toLowerCase(),
lienRequests: [
{
lienId: loanId,
tokenId: nftId,
},
],
};
const format = await this.post(`${config.blur.baseUrlBlend}/loan-offer/end`, payload);
// Sending the transaction
const tx = format.data.actions[0].txnData;
await this.client.sendTransaction(tx);
// const signedTx = await this.client.signTransaction(tx);
// await this.client.sendRawTransaction({ serializedTransaction: signedTx });
}
}