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
50 changes: 30 additions & 20 deletions src/blockchain/soroban/soroban.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as StellarSdk from 'stellar-sdk';
import { TransactionBuilderService } from '../../stellar/transaction-builder.service';

/**
* Low-level Soroban RPC client for interacting with smart contracts on the Stellar network.
Expand All @@ -12,7 +13,10 @@ export class SorobanService {
private readonly server: StellarSdk.SorobanRpc.Server;
private readonly networkPassphrase: string;

constructor(private readonly configService: ConfigService) {
constructor(
private readonly configService: ConfigService,
private readonly transactionBuilderService: TransactionBuilderService,
) {
const rpcUrl =
this.configService.get<string>('STELLAR_SOROBAN_URL') ||
'https://soroban-testnet.stellar.org';
Expand Down Expand Up @@ -55,28 +59,34 @@ export class SorobanService {

const account = new StellarSdk.Account(sourcePublic, '0');

const tx = new StellarSdk.TransactionBuilder(account, {
fee: '100',
networkPassphrase: this.networkPassphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(30)
.build();
return this.transactionBuilderService.buildWithFeeRetry(
1,
`${method} simulation`,
async (fee) => {
const tx = new StellarSdk.TransactionBuilder(account, {
fee,
networkPassphrase: this.networkPassphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(30)
.build();

const simulation = await this.server.simulateTransaction(tx);
const simulation = await this.server.simulateTransaction(tx);

if (StellarSdk.SorobanRpc.Api.isSimulationError(simulation)) {
const errorMsg =
(simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionErrorResponse).error ||
'Unknown simulation error';
throw new Error(`Soroban simulation failed: ${errorMsg}`);
}
if (StellarSdk.SorobanRpc.Api.isSimulationError(simulation)) {
const errorMsg =
(simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionErrorResponse).error ||
'Unknown simulation error';
throw new Error(`Soroban simulation failed: ${errorMsg}`);
}

const successResult = simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionSuccessResponse;
if (!successResult.result) {
throw new Error('Soroban simulation returned no result');
}
const successResult = simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionSuccessResponse;
if (!successResult.result) {
throw new Error('Soroban simulation returned no result');
}

return successResult.result.retval;
return successResult.result.retval;
},
);
}
}
7 changes: 6 additions & 1 deletion src/modules/transactions/transactions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConfigService } from '@nestjs/config';
import { Cache } from 'cache-manager';
import * as StellarSdk from 'stellar-sdk';
import { SupabaseService } from '../../database/supabase.client';
import { TransactionBuilderService } from '../../stellar/transaction-builder.service';
import { SubmitTransactionRequestDto, TransactionType } from './dto/submit-transaction-request.dto';
import { SubmitTransactionResponseDto } from './dto/submit-transaction-response.dto';
import {
Expand Down Expand Up @@ -71,6 +72,7 @@ export class TransactionsService {
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly configService: ConfigService,
private readonly supabaseService: SupabaseService,
private readonly transactionBuilderService: TransactionBuilderService,
) {
const horizonUrl =
this.configService.get<string>('STELLAR_HORIZON_URL') ||
Expand All @@ -92,7 +94,10 @@ export class TransactionsService {

let transactionHash: string;
try {
const horizonResult = await this.horizonServer.submitTransaction(transaction);
const horizonResult = await this.transactionBuilderService.submitWithFeeRetry(
transaction,
(retryTransaction) => this.horizonServer.submitTransaction(retryTransaction),
);
transactionHash = horizonResult.hash;
} catch (error) {
this.handleHorizonError(error);
Expand Down
68 changes: 41 additions & 27 deletions src/stellar/contracts/clients/creditline.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as StellarSdk from 'stellar-sdk';
import { SorobanService } from '../../../blockchain/soroban/soroban.service';
import { TransactionBuilderService } from '../../transaction-builder.service';
import { CreateLoanParams, CREDIT_LINE_CONTRACT_ID_KEY } from '../interfaces/creditline.interface';
import {
ContractNotConfiguredError,
Expand All @@ -15,6 +16,7 @@ export class CreditLineContractClient {

constructor(
private readonly sorobanService: SorobanService,
private readonly transactionBuilderService: TransactionBuilderService,
private readonly configService: ConfigService,
) {
this.contractId =
Expand Down Expand Up @@ -43,26 +45,32 @@ export class CreditLineContractClient {
const loanAmount = this.toContractAmount(params.loanAmount);
const guarantee = this.toContractAmount(params.guarantee);

const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase,
})
.addOperation(
contract.call(
'create_loan',
StellarSdk.nativeToScVal(params.loanId, { type: 'string' }),
StellarSdk.nativeToScVal(params.vendorId, { type: 'string' }),
StellarSdk.nativeToScVal(amount, { type: 'i128' }),
StellarSdk.nativeToScVal(loanAmount, { type: 'i128' }),
StellarSdk.nativeToScVal(guarantee, { type: 'i128' }),
StellarSdk.nativeToScVal(params.interestRate, { type: 'u32' }),
StellarSdk.nativeToScVal(params.term, { type: 'u32' }),
),
)
.setTimeout(30)
.build();
const prepared = await this.transactionBuilderService.buildWithFeeRetry(
1,
'create_loan transaction',
async (fee) => {
const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee,
networkPassphrase,
})
.addOperation(
contract.call(
'create_loan',
StellarSdk.nativeToScVal(params.loanId, { type: 'string' }),
StellarSdk.nativeToScVal(params.vendorId, { type: 'string' }),
StellarSdk.nativeToScVal(amount, { type: 'i128' }),
StellarSdk.nativeToScVal(loanAmount, { type: 'i128' }),
StellarSdk.nativeToScVal(guarantee, { type: 'i128' }),
StellarSdk.nativeToScVal(params.interestRate, { type: 'u32' }),
StellarSdk.nativeToScVal(params.term, { type: 'u32' }),
),
)
.setTimeout(30)
.build();

const prepared = await server.prepareTransaction(tx);
return server.prepareTransaction(tx);
},
);
return prepared.toXDR();
}

Expand All @@ -83,15 +91,21 @@ export class CreditLineContractClient {
const sourceKeypair = StellarSdk.Keypair.random();
const sourceAccount = new StellarSdk.Account(sourceKeypair.publicKey(), '0');

const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase,
})
.addOperation(contract.call('repay_loan', userArg, loanIdArg, amountArg))
.setTimeout(300)
.build();
const prepared = await this.transactionBuilderService.buildWithFeeRetry(
1,
'repay_loan transaction',
async (fee) => {
const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee,
networkPassphrase,
})
.addOperation(contract.call('repay_loan', userArg, loanIdArg, amountArg))
.setTimeout(300)
.build();

const prepared = await server.prepareTransaction(tx);
return server.prepareTransaction(tx);
},
);
return prepared.toXDR();
} catch (error) {
if (error instanceof ContractNotConfiguredError) {
Expand Down
96 changes: 62 additions & 34 deletions src/stellar/contracts/clients/liquidity-pool.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as StellarSdk from 'stellar-sdk';
import { SorobanService } from '../../../blockchain/soroban/soroban.service';
import { TransactionBuilderService } from '../../transaction-builder.service';
import { PoolStats, LIQUIDITY_POOL_CONTRACT_ID_KEY } from '../interfaces/liquidity-pool.interface';
import {
ContractNotConfiguredError,
Expand All @@ -20,6 +21,7 @@ export class LiquidityPoolContractClient {

constructor(
private readonly sorobanService: SorobanService,
private readonly transactionBuilderService: TransactionBuilderService,
private readonly configService: ConfigService,
) {
this.contractId =
Expand Down Expand Up @@ -172,23 +174,36 @@ export class LiquidityPoolContractClient {
const sourceKeypair = StellarSdk.Keypair.random();
const sourceAccount = new StellarSdk.Account(sourceKeypair.publicKey(), '0');

const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase,
})
.addOperation(contract.call('deposit', userArg, amountArg))
.setTimeout(300)
.build();

const simulation = await server.simulateTransaction(tx);

if (StellarSdk.SorobanRpc.Api.isSimulationError(simulation)) {
const errorMsg =
(simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionErrorResponse).error ||
'Unknown simulation error';
this.logger.error(`deposit simulation failed: ${errorMsg}`);
throw new ContractSimulationError('deposit');
}
const buildTransaction = (fee: string): StellarSdk.Transaction =>
new StellarSdk.TransactionBuilder(sourceAccount, {
fee,
networkPassphrase,
})
.addOperation(contract.call('deposit', userArg, amountArg))
.setTimeout(300)
.build();

const { tx, simulation } = await this.transactionBuilderService.buildWithFeeRetry(
1,
'deposit transaction',
async (fee) => {
const tx = buildTransaction(fee);
const simulation = await server.simulateTransaction(tx);

if (StellarSdk.SorobanRpc.Api.isSimulationError(simulation)) {
const errorMsg =
(simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionErrorResponse).error ||
'Unknown simulation error';
if (this.transactionBuilderService.isInsufficientFeeError(errorMsg)) {
throw new Error(errorMsg);
}
this.logger.error(`deposit simulation failed: ${errorMsg}`);
throw new ContractSimulationError('deposit');
}

return { tx, simulation };
},
);

const assembledTx = StellarSdk.SorobanRpc.assembleTransaction(
tx,
Expand Down Expand Up @@ -224,23 +239,36 @@ export class LiquidityPoolContractClient {
const sourceKeypair = StellarSdk.Keypair.random();
const sourceAccount = new StellarSdk.Account(sourceKeypair.publicKey(), '0');

const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase,
})
.addOperation(contract.call('withdraw', userArg, sharesArg))
.setTimeout(300)
.build();

const simulation = await server.simulateTransaction(tx);

if (StellarSdk.SorobanRpc.Api.isSimulationError(simulation)) {
const errorMsg =
(simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionErrorResponse).error ||
'Unknown simulation error';
this.logger.error(`withdraw simulation failed: ${errorMsg}`);
throw new ContractSimulationError('withdraw');
}
const buildTransaction = (fee: string): StellarSdk.Transaction =>
new StellarSdk.TransactionBuilder(sourceAccount, {
fee,
networkPassphrase,
})
.addOperation(contract.call('withdraw', userArg, sharesArg))
.setTimeout(300)
.build();

const { tx, simulation } = await this.transactionBuilderService.buildWithFeeRetry(
1,
'withdraw transaction',
async (fee) => {
const tx = buildTransaction(fee);
const simulation = await server.simulateTransaction(tx);

if (StellarSdk.SorobanRpc.Api.isSimulationError(simulation)) {
const errorMsg =
(simulation as StellarSdk.SorobanRpc.Api.SimulateTransactionErrorResponse).error ||
'Unknown simulation error';
if (this.transactionBuilderService.isInsufficientFeeError(errorMsg)) {
throw new Error(errorMsg);
}
this.logger.error(`withdraw simulation failed: ${errorMsg}`);
throw new ContractSimulationError('withdraw');
}

return { tx, simulation };
},
);

const assembledTx = StellarSdk.SorobanRpc.assembleTransaction(
tx,
Expand Down
6 changes: 6 additions & 0 deletions src/stellar/stellar.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SorobanService } from '../blockchain/soroban/soroban.service';
import { StellarService } from './stellar.service';
import { TransactionBuilderService } from './transaction-builder.service';
import { CreditLineContractClient } from './contracts/clients/creditline.client';
import { ReputationContractClient } from './contracts/clients/reputation.client';
import { LiquidityPoolContractClient } from './contracts/clients/liquidity-pool.client';
Expand All @@ -10,6 +12,8 @@ import { ParametersContractClient } from './contracts/clients/parameters.client'
@Module({
imports: [ConfigModule],
providers: [
StellarService,
TransactionBuilderService,
SorobanService,
CreditLineContractClient,
ReputationContractClient,
Expand All @@ -18,6 +22,8 @@ import { ParametersContractClient } from './contracts/clients/parameters.client'
ParametersContractClient,
],
exports: [
StellarService,
TransactionBuilderService,
SorobanService,
CreditLineContractClient,
ReputationContractClient,
Expand Down
31 changes: 31 additions & 0 deletions src/stellar/stellar.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as StellarSdk from 'stellar-sdk';

@Injectable()
export class StellarService {
private readonly logger = new Logger(StellarService.name);
private readonly horizonServer: StellarSdk.Horizon.Server;
private readonly networkPassphrase: string;

constructor(private readonly configService: ConfigService) {
const horizonUrl =
this.configService.get<string>('STELLAR_HORIZON_URL') ||
'https://horizon-testnet.stellar.org';

this.networkPassphrase =
this.configService.get<string>('STELLAR_NETWORK_PASSPHRASE') ||
StellarSdk.Networks.TESTNET;

this.horizonServer = new StellarSdk.Horizon.Server(horizonUrl);
this.logger.log(`Stellar Horizon client initialized: ${horizonUrl}`);
}

getHorizonServer(): StellarSdk.Horizon.Server {
return this.horizonServer;
}

getNetworkPassphrase(): string {
return this.networkPassphrase;
}
}
Loading
Loading