feat: boltz swap in refund address (#1490)

require a refund address when creating a swap in and paying it
externally to make sure the swap can be refunded automatically if it
fails.
This commit is contained in:
jackstar12 2025-05-07 22:35:52 +02:00 committed by GitHub
parent bad876466a
commit 8ba2d2be88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 66 additions and 12 deletions

View file

@ -79,7 +79,7 @@ export const getSwapInfo = (req, res, next) => {
});
};
export const createSwap = (req, res, next) => {
const { amount, sendFromInternal, address } = req.body;
const { amount, sendFromInternal, address, refundAddress } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Creating Swap..' });
options = common.getBoltzServerOptions(req);
if (options.url === '') {
@ -88,7 +88,7 @@ export const createSwap = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
options.url = options.url + '/v1/createswap';
options.body = { amount: amount };
options.body = { amount: amount, refundAddress };
if (sendFromInternal) {
options.body.send_from_internal = sendFromInternal;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -80,7 +80,7 @@ export const getSwapInfo = (req, res, next) => {
};
export const createSwap = (req, res, next) => {
const { amount, sendFromInternal, address } = req.body;
const { amount, sendFromInternal, address, refundAddress } = req.body;
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Boltz', msg: 'Creating Swap..' });
options = common.getBoltzServerOptions(req);
if (options.url === '') {
@ -89,7 +89,7 @@ export const createSwap = (req, res, next) => {
return res.status(err.statusCode).json({ message: err.message, error: err.error });
}
options.url = options.url + '/v1/createswap';
options.body = { amount: amount };
options.body = { amount: amount, refundAddress };
if (sendFromInternal) { options.body.send_from_internal = sendFromInternal; }
if (address && address !== '') { options.body.address = address; }
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Boltz', msg: 'Create Swap Options Body', data: options.body });

View file

@ -38,6 +38,16 @@
<mat-icon matTooltip="Pay from the node's onchain wallet" matTooltipPosition="above" class="info-icon mt-2">info_outline</mat-icon>
</div>
</div>
<div disabled="direction === swapTypeEnum.SWAP_IN && isSendFromInternalCompatible && !inputFormGroup?.controls?.sendFromInternal.value" fxLayout="column" fxFlex="100" fxLayoutAlign="start stretch" class="mt-1">
<mat-form-field fxLayout="column" fxFlex="100">
<mat-label>Refund Address</mat-label>
<input matInput type="text" tabindex="3" formControlName="refundAddress" [required]="!inputFormGroup?.controls?.sendFromInternal.value">
<mat-hint>The address where funds will be returned in case of a failed swap</mat-hint>
<mat-error *ngIf="inputFormGroup?.controls?.refundAddress?.errors?.required">
Refund address is required when not using internal wallet.
</mat-error>
</mat-form-field>
</div>
</div>
<div class="mt-2" fxLayout="row" fxLayoutAlign="start center" fxFlex="100">
<button *ngIf="direction === swapTypeEnum.SWAP_OUT" mat-button color="primary" tabindex="2" type="button" matStepperNext>Next</button>

View file

@ -56,7 +56,8 @@ export class SwapModalComponent implements OnInit, AfterViewInit, OnDestroy {
this.inputFormGroup = this.formBuilder.group({
amount: [this.serviceInfo.limits?.minimal, [Validators.required, Validators.min(this.serviceInfo.limits?.minimal || 0), Validators.max(this.serviceInfo.limits?.maximal || 0)]],
acceptZeroConf: [false],
sendFromInternal: [true]
sendFromInternal: [true],
refundAddress: [{ value: '', disabled: true }]
});
this.addressFormGroup = this.formBuilder.group({
addressType: ['local', [Validators.required]],
@ -89,6 +90,25 @@ export class SwapModalComponent implements OnInit, AfterViewInit, OnDestroy {
this.addressFormGroup.setErrors({ Invalid: true });
});
}
if (this.direction === SwapTypeEnum.SWAP_IN) {
this.inputFormGroup.controls.sendFromInternal.valueChanges.
pipe(takeUntil(this.unSubs[4])).
subscribe(() => {
this.onSendFromInternalChange();
});
}
}
onSendFromInternalChange() {
if (!this.inputFormGroup.controls.sendFromInternal.value) {
this.inputFormGroup.controls.refundAddress.enable();
this.inputFormGroup.controls.refundAddress.setValidators([Validators.required]);
this.inputFormGroup.controls.refundAddress.updateValueAndValidity();
} else {
this.inputFormGroup.controls.refundAddress.disable();
this.inputFormGroup.controls.refundAddress.clearValidators();
this.inputFormGroup.controls.refundAddress.updateValueAndValidity();
}
}
onAddressTypeChange(event: any) {
@ -115,7 +135,22 @@ export class SwapModalComponent implements OnInit, AfterViewInit, OnDestroy {
this.stepper.selected?.stepControl.setErrors(null);
this.stepper.next();
if (this.direction === SwapTypeEnum.SWAP_IN) {
this.boltzService.swapIn(this.inputFormGroup.controls.amount.value, this.isSendFromInternalCompatible ? this.inputFormGroup.controls.sendFromInternal.value : null).pipe(takeUntil(this.unSubs[2])).
const refundAddress = this.inputFormGroup.controls.sendFromInternal.value ?
null : this.inputFormGroup.controls.refundAddress.value;
const sendFromInternal = this.isSendFromInternalCompatible ? this.inputFormGroup.controls.sendFromInternal.value : null;
if (!sendFromInternal && !refundAddress) {
this.stepper.selected?.stepControl.setErrors({ Invalid: true });
this.flgEditable = true;
return;
}
this.boltzService.swapIn(
this.inputFormGroup.controls.amount.value,
sendFromInternal,
refundAddress
).pipe(takeUntil(this.unSubs[2])).
subscribe({
next: (swapStatus: CreateSwapResponse) => {
this.swapStatus = swapStatus;
@ -156,7 +191,11 @@ export class SwapModalComponent implements OnInit, AfterViewInit, OnDestroy {
case 1:
if (this.inputFormGroup.controls.amount.value) {
if (this.direction === SwapTypeEnum.SWAP_IN) {
this.inputFormLabel = this.swapDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Send from Internal Wallet: ' + (this.inputFormGroup.controls.sendFromInternal.value ? 'Yes' : 'No');
let summary = this.swapDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Send from Internal Wallet: ' + (this.inputFormGroup.controls.sendFromInternal.value ? 'Yes' : 'No');
if (!this.inputFormGroup.controls.sendFromInternal.value && this.inputFormGroup.controls.refundAddress.value) {
summary += ' | Refund Address: ' + this.inputFormGroup.controls.refundAddress.value;
}
this.inputFormLabel = summary;
} else {
this.inputFormLabel = this.swapDirectionCaption + ' Amount: ' + (this.decimalPipe.transform(this.inputFormGroup.controls.amount.value ? this.inputFormGroup.controls.amount.value : 0)) + ' Sats | Zero Conf: ' + (this.inputFormGroup.controls.acceptZeroConf.value ? 'Yes' : 'No');
}
@ -201,7 +240,12 @@ export class SwapModalComponent implements OnInit, AfterViewInit, OnDestroy {
onRestart() {
this.stepper.reset();
this.flgEditable = true;
this.inputFormGroup.reset({ amount: this.serviceInfo.limits?.minimal, acceptZeroConf: false, sendFromInternal: true });
this.inputFormGroup.reset({
amount: this.serviceInfo.limits?.minimal,
acceptZeroConf: false,
sendFromInternal: true,
refundAddress: ''
});
this.statusFormGroup.reset();
this.addressFormGroup.reset({ addressType: 'local', address: '' });
this.addressFormGroup.controls.address.disable();

View file

@ -85,8 +85,8 @@ export class BoltzService implements OnDestroy {
return this.httpClient.post(this.swapUrl, requestBody).pipe(catchError((err) => this.handleErrorWithoutAlert('Swap Out for Address: ' + address, UI_MESSAGES.NO_SPINNER, err)));
}
swapIn(amount: number, sendFromInternal: boolean) {
const requestBody = { amount: amount, sendFromInternal: sendFromInternal };
swapIn(amount: number, sendFromInternal: boolean, refundAddress?: string) {
const requestBody = { amount: amount, sendFromInternal: sendFromInternal, refundAddress };
this.swapUrl = API_URL + API_END_POINTS.BOLTZ_API + '/createswap';
return this.httpClient.post(this.swapUrl, requestBody).pipe(catchError((err) => this.handleErrorWithoutAlert('Swap In for Amount: ' + amount, UI_MESSAGES.NO_SPINNER, err)));
}