diff --git a/apps/api/src/solowallet/solowallet.controller.ts b/apps/api/src/solowallet/solowallet.controller.ts index 08b84bb..38acc41 100644 --- a/apps/api/src/solowallet/solowallet.controller.ts +++ b/apps/api/src/solowallet/solowallet.controller.ts @@ -1,4 +1,5 @@ import { + ContinueTxRequestDto, DepositFundsRequestDto, UpdateTxDto, UserTxsRequestDto, @@ -51,4 +52,13 @@ export class SolowalletController { updateShares(@Body() req: UpdateTxDto) { return this.walletService.updateTransaction(req); } + + @Post('continue') + @ApiOperation({ summary: 'Continue Solowallet transaction' }) + @ApiBody({ + type: ContinueTxRequestDto, + }) + continueTransaction(@Body() req: ContinueTxRequestDto) { + return this.walletService.continueTransaction(req); + } } diff --git a/apps/api/src/solowallet/solowallet.service.ts b/apps/api/src/solowallet/solowallet.service.ts index b80df3f..e702c9b 100644 --- a/apps/api/src/solowallet/solowallet.service.ts +++ b/apps/api/src/solowallet/solowallet.service.ts @@ -5,6 +5,7 @@ import { SolowalletServiceClient, WithdrawFundsRequestDto, UpdateTxDto, + ContinueTxRequestDto, } from '@bitsacco/common'; import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { type ClientGrpc } from '@nestjs/microservices'; @@ -38,4 +39,8 @@ export class SolowalletService implements OnModuleInit { updateTransaction(req: UpdateTxDto) { return this.client.updateTransaction(req); } + + continueTransaction(req: ContinueTxRequestDto) { + return this.client.continueTransaction(req); + } } diff --git a/apps/solowallet/src/solowallet.controller.ts b/apps/solowallet/src/solowallet.controller.ts index 5354bd4..eb3756f 100644 --- a/apps/solowallet/src/solowallet.controller.ts +++ b/apps/solowallet/src/solowallet.controller.ts @@ -6,6 +6,7 @@ import { UserTxsRequestDto, WithdrawFundsRequestDto, UpdateTxDto, + ContinueTxRequestDto, } from '@bitsacco/common'; import { SolowalletService } from './solowallet.service'; @@ -33,4 +34,9 @@ export class SolowalletController { updateTransaction(request: UpdateTxDto) { return this.solowalletService.updateTransaction(request); } + + @GrpcMethod() + continueTransaction(request: ContinueTxRequestDto) { + return this.solowalletService.continueTransaction(request); + } } diff --git a/apps/solowallet/src/solowallet.service.ts b/apps/solowallet/src/solowallet.service.ts index e913e4b..dbb4365 100644 --- a/apps/solowallet/src/solowallet.service.ts +++ b/apps/solowallet/src/solowallet.service.ts @@ -26,6 +26,7 @@ import { WithdrawFundsRequestDto, CreateOfframpSwapDto, UpdateTxDto, + ContinueTxRequestDto, } from '@bitsacco/common'; import { type ClientGrpc } from '@nestjs/microservices'; import { catchError, firstValueFrom, map, of, tap } from 'rxjs'; @@ -71,7 +72,7 @@ export class SolowalletService { }) .pipe( tap((quote: QuoteResponse) => { - this.logger.log(`Quote: ${quote}`); + this.logger.log(`Quote: ${JSON.stringify(quote)}`); }), map((quote: QuoteResponse) => { const { amountMsats } = fiatToBtc({ @@ -114,7 +115,7 @@ export class SolowalletService { .createOnrampSwap(fiatDeposit) .pipe( tap((swap: SwapResponse) => { - this.logger.log(`Swap: ${swap}`); + this.logger.log(`Swap: ${JSON.stringify(swap)}`); }), map((swap: SwapResponse) => { const { amountMsats } = fiatToBtc({ @@ -330,7 +331,7 @@ export class SolowalletService { status: TransactionStatus.PENDING, }; - this.logger.log(status); + this.logger.log(`Status: ${status}`); const deposit = await this.wallet.create({ userId, amountMsats, @@ -490,6 +491,79 @@ export class SolowalletService { }; } + async continueTransaction({ + userId, + txId, + amountFiat, + onramp, + }: ContinueTxRequestDto): Promise { + const tx = await this.wallet.findOne({ _id: txId }); + + if (tx.userId !== userId) { + throw 'Invalid request to continue transaction'; + } + + const { quote, amountMsats } = await this.getQuote({ + from: onramp?.currency || Currency.KES, + to: Currency.BTC, + amount: amountFiat.toString(), + }); + + const lightning = await this.fedimintService.invoice( + amountMsats, + tx.reference, + ); + + const { status } = onramp + ? await this.initiateOnrampSwap({ + quote, + amountFiat: amountFiat.toString(), + reference: tx.reference, + source: onramp, + target: { + payout: lightning, + }, + }) + : { + status: TransactionStatus.PENDING, + }; + + this.logger.log(`Status: ${status}`); + const deposit = await this.wallet.findOneAndUpdate( + { + _id: txId, + userId, + }, + { + amountMsats, + amountFiat, + lightning: JSON.stringify(lightning), + paymentTracker: lightning.operationId, + status, + }, + ); + + // listen for payment + this.fedimintService.receive( + ReceiveContext.SOLOWALLET, + lightning.operationId, + ); + + const ledger = await this.getPaginatedUserTxLedger({ + userId, + pagination: { page: 0, size: 10 }, + }); + + const meta = await this.getWalletMeta(userId); + + return { + txId: deposit._id, + ledger, + meta, + userId, + }; + } + @OnEvent(fedimint_receive_success) private async handleSuccessfulReceive({ context, diff --git a/libs/common/src/database/abstract.repository.ts b/libs/common/src/database/abstract.repository.ts index 7b47537..e66d408 100644 --- a/libs/common/src/database/abstract.repository.ts +++ b/libs/common/src/database/abstract.repository.ts @@ -3,7 +3,6 @@ import { Model, PipelineStage, SortOrder, - Types, UpdateQuery, } from 'mongoose'; import { Logger, NotFoundException } from '@nestjs/common'; diff --git a/libs/common/src/dto/solowallet.dto.ts b/libs/common/src/dto/solowallet.dto.ts index ac0a5f4..f3177dd 100644 --- a/libs/common/src/dto/solowallet.dto.ts +++ b/libs/common/src/dto/solowallet.dto.ts @@ -22,6 +22,7 @@ import { UpdateTxRequest, SolowalletTxUpdates, TransactionStatus, + ContinueTxRequest, } from '../types'; import { PaginatedRequestDto } from './lib.dto'; @@ -125,3 +126,28 @@ export class UpdateTxDto implements UpdateTxRequest { @ApiProperty({ type: SolowalletTxUpdatesDto }) updates: SolowalletTxUpdatesDto; } + +export class ContinueTxRequestDto implements ContinueTxRequest { + @IsNotEmpty() + @IsString() + @Type(() => String) + @ApiProperty({ example: '7b158dfd-cb98-40b1-9ed2-a13006a9f670' }) + userId: string; + + @IsNotEmpty() + @IsString() + @Type(() => String) + @ApiProperty({ example: '35f47ebd-599e-4334-a741-67f3495995e3' }) + txId: string; + + @ApiProperty() + @IsNumber() + @Min(1) + @ApiProperty({ example: 2 }) + amountFiat: number; + + @ValidateNested() + @Type(() => OnrampSwapSourceDto) + @ApiProperty({ type: OnrampSwapSourceDto }) + onramp?: OnrampSwapSourceDto; +} diff --git a/libs/common/src/types/proto/solowallet.ts b/libs/common/src/types/proto/solowallet.ts index 089e871..08df14b 100644 --- a/libs/common/src/types/proto/solowallet.ts +++ b/libs/common/src/types/proto/solowallet.ts @@ -85,6 +85,13 @@ export interface SolowalletTxUpdates { reference?: string | undefined; } +export interface ContinueTxRequest { + userId: string; + txId: string; + amountFiat: number; + onramp?: OnrampSwapSource | undefined; +} + export interface SolowalletServiceClient { depositFunds(request: DepositFundsRequest): Observable; @@ -93,6 +100,8 @@ export interface SolowalletServiceClient { userTransactions(request: UserTxsRequest): Observable; updateTransaction(request: UpdateTxRequest): Observable; + + continueTransaction(request: ContinueTxRequest): Observable; } export interface SolowalletServiceController { @@ -111,6 +120,10 @@ export interface SolowalletServiceController { updateTransaction( request: UpdateTxRequest, ): Promise | Observable | UserTxsResponse; + + continueTransaction( + request: ContinueTxRequest, + ): Promise | Observable | UserTxsResponse; } export function SolowalletServiceControllerMethods() { @@ -120,6 +133,7 @@ export function SolowalletServiceControllerMethods() { 'withdrawFunds', 'userTransactions', 'updateTransaction', + 'continueTransaction', ]; for (const method of grpcMethods) { const descriptor: any = Reflect.getOwnPropertyDescriptor( diff --git a/proto/solowallet.proto b/proto/solowallet.proto index 2f6fedd..bc1c172 100644 --- a/proto/solowallet.proto +++ b/proto/solowallet.proto @@ -14,6 +14,8 @@ service SolowalletService { rpc UserTransactions(UserTxsRequest) returns (UserTxsResponse){} rpc UpdateTransaction(UpdateTxRequest) returns (UserTxsResponse) {} + + rpc ContinueTransaction(ContinueTxRequest) returns (UserTxsResponse) {} } message DepositFundsRequest { @@ -110,3 +112,13 @@ message SolowalletTxUpdates { optional lightning.Bolt11 lightning = 2; optional string reference = 3; } + +message ContinueTxRequest { + string user_id = 1; + + string tx_id = 2; + + int32 amount_fiat = 3; + + optional swap.OnrampSwapSource onramp = 5; +}