import { Connection, Cluster, Keypair, clusterApiUrl, PublicKey, sendAndConfirmTransaction, SystemProgram, Transaction, SYSVAR_RENT_PUBKEY, TransactionInstruction, } from "@solana/web3.js"; import BN from "bn.js"; import * as borsh from "borsh"; enum Instructions { Initialize = 0, PayRent, TerminateEarly, } enum AgreementStatus { Uninitialized = 0, Active, Completed, Violated, } enum RentalDuration { Months = 0, } export type ErrorData = { error: { name: string; message: string; }; }; export type RentShareAccountData = { payeePublicKey: PublicKey; payerPublicKey: PublicKey; deposit: number; rentAmount: number; duration: number; durationUnit: number; remainingPayments: number; }; class RentShareAccount { status: AgreementStatus; payeePublicKey: number[]; payerPublicKey: number[]; deposit = 0; rentAmount = 0; duration = 0; durationUnit = 0; remainingPayments = 0; constructor(data: RentShareAccountData) { this.status = AgreementStatus.Uninitialized; if (data.payeePublicKey instanceof PublicKey) { this.payeePublicKey = Array.from(data.payeePublicKey.toBytes()); } else { this.payeePublicKey = Array.from(data.payeePublicKey); } if (data.payerPublicKey instanceof PublicKey) { this.payerPublicKey = Array.from(data.payerPublicKey.toBytes()); } else { this.payerPublicKey = Array.from(data.payerPublicKey); } this.deposit = data.deposit; this.rentAmount = data.rentAmount; this.duration = data.duration; this.durationUnit = data.durationUnit; this.remainingPayments = data.remainingPayments; } } /** * Borsh schema definition for rent share account data */ const RentShareDataSchema = new Map([ [ RentShareAccount, { kind: "struct", fields: [ ["status", "u8"], ["payeePublicKey", [32]], ["payerPublicKey", [32]], ["deposit", "u64"], ["rentAmount", "u64"], ["duration", "u64"], ["durationUnit", "u8"], ["remainingPayments", "u64"], ], }, ], ]); /** * The expected size of each rent share account. */ const RENT_SCHEMA_SIZE = borsh.serialize( RentShareDataSchema, new RentShareAccount({ payeePublicKey: PublicKey.default, payerPublicKey: PublicKey.default, deposit: 0, rentAmount: 0, duration: 0, durationUnit: 0, remainingPayments: 0, }) ).length; /** * Create Rent Share account if it doesn't already exist */ export async function createRentShareAccount( accountOwner: Keypair, seed: string ): Promise { const programId = new PublicKey(process.env.PROGRAM_ID || ""); const clusterUrl = clusterApiUrl(process.env.solanaEnvironment as Cluster); const connection = new Connection(clusterUrl, "confirmed"); const rentAgreementPublicKey = await PublicKey.createWithSeed( accountOwner.publicKey, seed, programId ); const rentShareAccountInfo = await connection.getAccountInfo( rentAgreementPublicKey ); if (rentShareAccountInfo === null) { const lamports = await connection.getMinimumBalanceForRentExemption( RENT_SCHEMA_SIZE ); console.log( `Lamports for rent: ${lamports}. Schema size: ${RENT_SCHEMA_SIZE}` ); const transaction = new Transaction().add( SystemProgram.createAccountWithSeed({ fromPubkey: accountOwner.publicKey, basePubkey: accountOwner.publicKey, seed, newAccountPubkey: rentAgreementPublicKey, lamports, space: RENT_SCHEMA_SIZE, programId, }) ); await sendAndConfirmTransaction(connection, transaction, [accountOwner]); } return rentAgreementPublicKey; } /** * Initialize a rental agreement between a payer and payee specified in account data */ export async function initializeRentShareAccount( rentAgreementPublicKey: PublicKey, accountOwner: Keypair, data: RentShareAccountData ): Promise { const programId = new PublicKey(process.env.PROGRAM_ID || ""); const clusterUrl = clusterApiUrl(process.env.solanaEnvironment as Cluster); const connection = new Connection(clusterUrl, "confirmed"); const instruction = Instructions.Initialize; const transactionInstruction = new TransactionInstruction({ keys: [ { pubkey: rentAgreementPublicKey, isSigner: false, isWritable: true }, { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, ], programId, data: Buffer.from( Uint8Array.of( instruction, ...Array.from(data.payeePublicKey.toBytes()), ...Array.from(data.payerPublicKey.toBytes()), ...new BN(data.deposit).toArray("le", 8), ...new BN(data.rentAmount).toArray("le", 8), ...new BN(data.duration).toArray("le", 8), data.durationUnit ) ), }); const signature = await sendAndConfirmTransaction( connection, new Transaction().add(transactionInstruction), [accountOwner] ); } /** * Record a rent transaction between a payer and payee */ export async function recordRentPayment( rentAgreementPublicKey: PublicKey, payerPrivateKey: Keypair, payeePrivateKey: Keypair, rentAmount: number ): Promise { const programId = new PublicKey(process.env.PROGRAM_ID || ""); const clusterUrl = clusterApiUrl(process.env.solanaEnvironment as Cluster); const connection = new Connection(clusterUrl, "confirmed"); const instruction = Instructions.PayRent; const transactionInstruction = new TransactionInstruction({ keys: [ { pubkey: rentAgreementPublicKey, isSigner: false, isWritable: true }, { pubkey: payeePrivateKey.publicKey, isSigner: false, isWritable: true }, { pubkey: payerPrivateKey.publicKey, isSigner: true, isWritable: true }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: true }, ], programId, data: Buffer.from( Uint8Array.of(instruction, ...new BN(rentAmount).toArray("le", 8)) ), }); const signature = await sendAndConfirmTransaction( connection, new Transaction().add(transactionInstruction), [payerPrivateKey] ); return signature; }