Repository: 40acres/40swap
Pull request #123: Add frontend support on swap in for liquid
Author: login: alejandroacho
Created: 2025-05-05T12:15:45Z | Merged: 2025-05-13T08:34:29Z

## 📄 Description

This PR introduces support for the Liquid network in swap-in operations. The changes enable users to perform swaps involving both Bitcoin and Liquid networks, enhancing the platform's versatility.

**Liquid Swap-In Support:**

- Added Liquid network support for swap-in operations, including transaction building, broadcasting, and event handling.
- Refactored SwapInRunner, SwapService, and related classes to handle both Bitcoin and Liquid chains.
- Updated NBXplorer event schemas and types to support Liquid and Bitcoin transaction outputs.
- Updated swap forms and history components to support and display Liquid swaps.
- Improved swap type and chain icon logic for better user clarity.
- Added success messages for Liquid-to-Lightning swaps.
- Added config needed for liquid membpool url


## 🚀 Demo

Happy path:

https://github.com/user-attachments/assets/a1b309a8-d2af-423f-9819-6edbe75df0af 

---

Refund path:

https://github.com/user-attachments/assets/3d6e3dd1-776d-4815-85c5-1a98ac04aa6e




Diff:
diff --git a/docker/nodes-setup.sh b/docker/nodes-setup.sh
index 5b2ba408..cbdedb48 100755
--- a/docker/nodes-setup.sh
+++ b/docker/nodes-setup.sh
@@ -105,6 +105,7 @@ elements:
   rpcUsername: 40swap
   rpcPassword: pass
   xpub: $xpub
+  esploraUrl: http://localhost:35000
 EOM
 
 echo "$xpub_config" > ../server-backend/dev/40swap.elements.yml
diff --git a/server-backend/src/ConfigurationController.ts b/server-backend/src/ConfigurationController.ts
index b267baf1..4967e37e 100644
--- a/server-backend/src/ConfigurationController.ts
+++ b/server-backend/src/ConfigurationController.ts
@@ -16,6 +16,7 @@ export class ConfigurationController {
             minimumAmount: this.config.getOrThrow('swap.minimumAmount', { infer: true }),
             maximumAmount: this.config.getOrThrow('swap.maximumAmount', { infer: true }),
             mempoolDotSpaceUrl: this.config.getOrThrow('mempoolBlockExplorer.url', { infer: true }),
+            esploraUrl: this.config.getOrThrow('elements.esploraUrl', { infer: true }),
         };
     }
 }
\ No newline at end of file
diff --git a/server-backend/src/LiquidService.ts b/server-backend/src/LiquidService.ts
index d0dca05c..635655d5 100644
--- a/server-backend/src/LiquidService.ts
+++ b/server-backend/src/LiquidService.ts
@@ -13,6 +13,7 @@ const LiquidConfigurationDetailsSchema = z.object({
         password: z.string(),
     }),
     xpub: z.string(),
+    esploraUrl: z.string(),
 });
 
 export type LiquidConfigurationDetails = z.infer<typeof LiquidConfigurationDetailsSchema>;
@@ -71,7 +72,7 @@ export class LiquidService implements OnApplicationBootstrap  {
     private readonly logger = new Logger(LiquidService.name);
     private readonly rpcUrl: string;
     private readonly rpcAuth: {username: string, password: string};
-    
+
     constructor(
         private nbxplorer: NbxplorerService,
         @Inject('ELEMENTS_CONFIG') private elementsConfig: FourtySwapConfiguration['elements'],
@@ -86,6 +87,7 @@ export class LiquidService implements OnApplicationBootstrap  {
             rpcUrl: this.rpcUrl,
             rpcAuth: this.rpcAuth,
             xpub: this.xpub,
+            esploraUrl: this.elementsConfig.esploraUrl,
         });
     }
     
diff --git a/server-backend/src/SwapInController.ts b/server-backend/src/SwapInController.ts
index 0b0ed3e3..322bd6cc 100644
--- a/server-backend/src/SwapInController.ts
+++ b/server-backend/src/SwapInController.ts
@@ -169,6 +169,7 @@ export class SwapInController {
             rpcUrl: this.liquidService.configurationDetails.rpcUrl,
             rpcUsername: this.liquidService.configurationDetails.rpcAuth.username,
             rpcPassword: this.liquidService.configurationDetails.rpcAuth.password,
+            esploraUrl: this.liquidService.configurationDetails.esploraUrl,
         }, network);
         const pset = await psetBuilder.getPset(swap, liquid.Transaction.fromBuffer(swap.lockTx!), outputAddress);
         return pset;
diff --git a/server-backend/src/SwapOutController.ts b/server-backend/src/SwapOutController.ts
index 87732711..6efe8ed2 100644
--- a/server-backend/src/SwapOutController.ts
+++ b/server-backend/src/SwapOutController.ts
@@ -165,6 +165,7 @@ export class SwapOutController {
             rpcUrl: this.liquidService.configurationDetails.rpcUrl,
             rpcUsername: this.liquidService.configurationDetails.rpcAuth.username,
             rpcPassword: this.liquidService.configurationDetails.rpcAuth.password,
+            esploraUrl: this.liquidService.configurationDetails.esploraUrl,
         }, liquidNetwork);
         const lockTx = liquid.Transaction.fromBuffer(swap.lockTx!);
         return await psetBuilder.getPset(swap, lockTx, destinationAddress);
diff --git a/server-backend/src/configuration.ts b/server-backend/src/configuration.ts
index 347dc71e..f37e76d0 100644
--- a/server-backend/src/configuration.ts
+++ b/server-backend/src/configuration.ts
@@ -64,6 +64,7 @@ export const configSchema = z.object({
         rpcUrl: z.string().url(),
         rpcUsername: z.string(),
         rpcPassword: z.string(),
+        esploraUrl: z.string().url(),
     }),
 });
 
diff --git a/server-backend/test/BackendRestClient.ts b/server-backend/test/BackendRestClient.ts
index 920ac17b..f143e616 100644
--- a/server-backend/test/BackendRestClient.ts
+++ b/server-backend/test/BackendRestClient.ts
@@ -26,6 +26,7 @@ export class BackendRestClient {
             minimumAmount: 1000,
             maximumAmount: 100000000,
             mempoolDotSpaceUrl: 'https://mempool.space',
+            esploraUrl: 'https://blockstream.info/liquid',
         });
 
     }
diff --git a/server-backend/test/integration.test.ts b/server-backend/test/integration.test.ts
index 49ca8000..93f01830 100644
--- a/server-backend/test/integration.test.ts
+++ b/server-backend/test/integration.test.ts
@@ -342,6 +342,7 @@ describe('40Swap backend', () => {
                 rpcUrl: 'http://elements:18884',
                 rpcUsername: '40swap',
                 rpcPassword: 'pass',
+                esploraUrl: 'http://localhost:3000',
                 xpub,
             },
         };
diff --git a/shared/src/api.types.ts b/shared/src/api.types.ts
index 0e002342..cfd94ecd 100644
--- a/shared/src/api.types.ts
+++ b/shared/src/api.types.ts
@@ -103,6 +103,7 @@ export const frontendConfigurationSchema = z.object({
     minimumAmount: z.number(),
     maximumAmount: z.number(),
     mempoolDotSpaceUrl: z.string().url(),
+    esploraUrl: z.string().url(),
 });
 export type FrontendConfiguration = z.infer<typeof frontendConfigurationSchema>;
 export type FrontendConfigurationServer = z.input<typeof frontendConfigurationSchema>;
diff --git a/swap-frontend/src/History.tsx b/swap-frontend/src/History.tsx
index a8b80d0f..1d682845 100644
--- a/swap-frontend/src/History.tsx
+++ b/swap-frontend/src/History.tsx
@@ -79,6 +79,7 @@ export const History: Component = () => {
                     <tr>
                         <th>Type</th>
                         <th>Id</th>
+                        <th>Chain</th>
                         <th>Date</th>
                         <th>Actions</th>
                     </tr>
@@ -88,6 +89,7 @@ export const History: Component = () => {
                         <tr>
                             <td><SwapTypeComponent type={s.type} chain={s.chain} /></td>
                             <td><A href={`/swap/${s.type}/${s.swapId}`}>{s.swapId}</A></td>
+                            <td>{s.chain}</td>
                             <td>{new Date(s.createdAt).toLocaleString()}</td>
                             <td><span onClick={() => setSwapToDelete(s.swapId)}><Fa icon={faTrash} /></span></td>
                         </tr>
diff --git a/swap-frontend/src/LocalSwapStorageService.ts b/swap-frontend/src/LocalSwapStorageService.ts
index 569bb57b..59aabce4 100644
--- a/swap-frontend/src/LocalSwapStorageService.ts
+++ b/swap-frontend/src/LocalSwapStorageService.ts
@@ -51,10 +51,29 @@ export class LocalSwapStorageService {
                 console.error('PERSISTED DATA ERROR', error);
             }
         }
-        this.db = idb.openDB<FourtySwapDbSchema>('40swap', 1, {
-            upgrade(db) {
-                const store = db.createObjectStore('swap', { keyPath: 'swapId' });
-                store.createIndex('by-created-at', 'createdAt', { unique: false });
+        this.db = idb.openDB<FourtySwapDbSchema>('40swap', 2, {
+            upgrade(db, oldVersion, newVersion, transaction) {
+                const store = db.objectStoreNames.contains('swap')
+                    ? transaction.objectStore('swap')
+                    : db.createObjectStore('swap', { keyPath: 'swapId' });
+        
+                if (oldVersion < 1) {
+                    store.createIndex('by-created-at', 'createdAt', { unique: false });
+                }
+        
+                if (oldVersion < 2) {
+                    const index = store.index('by-created-at');
+                    index.getAll().then((swaps: (PersistedSwapIn | PersistedSwapOut)[]) => {
+                        for (const swap of swaps) {
+                            if (!swap.chain) {
+                                swap.chain = 'BITCOIN';
+                                store.put(swap);
+                            }
+                        }
+                    }).catch(err => {
+                        console.error('Migration failed', err);
+                    });
+                }
             },
         });
     }
diff --git a/swap-frontend/src/SwapForm.tsx b/swap-frontend/src/SwapForm.tsx
index e5d22a3b..029aa66c 100644
--- a/swap-frontend/src/SwapForm.tsx
+++ b/swap-frontend/src/SwapForm.tsx
@@ -205,7 +205,8 @@ export const SwapForm: Component = () => {
         }
         try {
             if (swapType() === 'in') {
-                const swap = await swapInService.createSwap(form.payload);
+                const chain = form.from === 'ON_CHAIN_BITCOIN' ? 'BITCOIN' : 'LIQUID';
+                const swap = await swapInService.createSwap(form.payload, chain);
                 navigate(`/swap/in/${swap.swapId}`);
             } else if (swapType() === 'out') {
                 const chain = form.to === 'ON_CHAIN_BITCOIN' ? 'BITCOIN' : 'LIQUID';
diff --git a/swap-frontend/src/SwapInDetails.tsx b/swap-frontend/src/SwapInDetails.tsx
index 453e1e2b..da75a01e 100644
--- a/swap-frontend/src/SwapInDetails.tsx
+++ b/swap-frontend/src/SwapInDetails.tsx
@@ -10,11 +10,14 @@ import bitcoinLogo from '/assets/bitcoin-logo.svg';
 import successImage from '/assets/success-image.png';
 import failureImage from '/assets/failure-image.png';
 import lockOpenImage from '/assets/lock-open.svg';
+import liquidLogo from '/assets/liquid-logo.svg';
 import { createTimer } from '@solid-primitives/timer';
 import { Spinner } from './Spinner.js';
 import { ActionButton } from './ActionButton.js';
 import { currencyFormat, jsonEquals } from './utils.js';
 import { toast } from 'solid-toast';
+import * as liquid from 'liquidjs-lib';
+import { getLiquidNetworkFromBitcoinNetwork } from '@40swap/shared';
 
 export const SwapInDetails: Component = () => {
     const { swapInService, localSwapStorageService } = applicationContext;
@@ -40,12 +43,22 @@ export const SwapInDetails: Component = () => {
             return false;
         }
         const network = config()?.bitcoinNetwork ?? networks.bitcoin;
-        try {
-            address.toOutputScript(refundAddress(), network);
-            return false;
-        } catch (e) {
-            return true;
+        if (currentSwap()?.chain === 'BITCOIN') {
+            try {
+                address.toOutputScript(refundAddress(), network);
+                return false;
+            } catch (e) {
+                return true;
+            }
+        } else if (currentSwap()?.chain === 'LIQUID') {
+            try {
+                liquid.address.toOutputScript(refundAddress(), getLiquidNetworkFromBitcoinNetwork(network));
+                return false;
+            } catch (e) {
+                return true;
+            }
         }
+        return false;
     }
 
     async function startRefund(): Promise<void> {
@@ -64,14 +77,19 @@ export const SwapInDetails: Component = () => {
     }
 
     const bip21Address = (): string => `bitcoin:${currentSwap()?.contractAddress}?amount=${currentSwap()?.inputAmount}`;
+    const liquidNetwork = getLiquidNetworkFromBitcoinNetwork(config()?.bitcoinNetwork ?? networks.bitcoin);
+    const liquidBip21Address = (): string => `liquidnetwork:${currentSwap()?.contractAddress}?amount=${currentSwap()?.inputAmount}&assetid=${liquidNetwork.assetHash}`;
 
     return <>
         <Switch
-            fallback={<h3 class="fw-bold">Swap bitcoin to lightning</h3>}
+            fallback={<h3 class="fw-bold">Swap {currentSwap()?.chain.toLowerCase()} to lightning</h3>}
         >
-            <Match when={currentSwap()?.status === 'DONE' && currentSwap()?.outcome === 'SUCCESS'}>
+            <Match when={currentSwap()?.status === 'DONE' && currentSwap()?.outcome === 'SUCCESS' && currentSwap()?.chain === 'BITCOIN'}>
                 <h3 class="text-center" style="text-transform: none">You have successfully swapped Bitcoin to Lightning!</h3>
             </Match>
+            <Match when={currentSwap()?.status === 'DONE' && currentSwap()?.outcome === 'SUCCESS' && currentSwap()?.chain === 'LIQUID'}>
+                <h3 class="text-center" style="text-transform: none">You have successfully swapped Liquid to Lightning!</h3>
+            </Match>
             <Match when={currentSwap()?.status === 'DONE' && currentSwap()?.outcome === 'REFUNDED'}>
                 <h3 class="text-center" style="text-transform: none">Transaction failed. Please try again.</h3>
             </Match>
@@ -187,7 +205,7 @@ export const SwapInDetails: Component = () => {
                         <div class="text-muted">Completing the swap</div>
                     </div>
                 }>
-                    <Match when={s().status === 'CREATED'}>
+                    <Match when={s().status === 'CREATED' && s().chain === 'BITCOIN'}>
                         <div class="d-flex justify-content-center">
                             <QrCode data={bip21Address()} image={bitcoinLogo}/>
                         </div>
@@ -201,13 +219,29 @@ export const SwapInDetails: Component = () => {
                             </Button>
                         </div>
                     </Match>
+                    <Match when={s().status === 'CREATED' && s().chain === 'LIQUID'}>
+                        <div class="d-flex justify-content-center">
+                            <QrCode data={liquidBip21Address()} image={liquidLogo}/>
+                        </div>
+                        <div class="d-flex flex-grow-1 flex-shrink-0 gap-2">
+                            <a href={liquidBip21Address()} class="btn btn-primary" role="button">Pay</a>
+                            <Button onclick={() => navigator.clipboard.writeText(s().inputAmount.toString())}>
+                                <Fa icon={faCopy}/> Copy amount
+                            </Button>
+                            <Button onclick={() => navigator.clipboard.writeText(s().contractAddress.toString())}>
+                                <Fa icon={faCopy}/> Copy address
+                            </Button>
+                        </div>
+                    </Match>
                     <Match when={s().status === 'DONE'}>
                         <A href="/" class="btn btn-primary"><Fa icon={faArrowRotateBack}/> Start new swap</A>
                     </Match>
                     <Match when={s().status === 'CONTRACT_EXPIRED' && s().refundRequestDate == null}>
                         <div>
                             <Form.Group class="mb-3">
-                                <Form.Control type="text" placeholder="Enter bitcoin address to receive refund"
+                                <Form.Control 
+                                    type="text" 
+                                    placeholder={`Enter ${s().chain.toLowerCase()} address to receive refund`}
                                     value={refundAddress()}
                                     onChange={e => setRefundAddress(e.target.value)}
                                     onKeyUp={e => setRefundAddress(e.currentTarget.value)}
@@ -218,11 +252,16 @@ export const SwapInDetails: Component = () => {
                         </div>
                     </Match>
                 </Switch>
-                <Show when={s().contractAddress}>
+                <Show when={s().contractAddress && s().chain === 'BITCOIN'}>
                     <a class="action-link" href={`${config()?.mempoolDotSpaceUrl}/address/${s().contractAddress}`}>
                         <img src={lockOpenImage} class="me-2" />Open lockup address
                     </a>
                 </Show>
+                <Show when={s().contractAddress && s().chain === 'LIQUID'}>
+                    <a class="action-link" href={`${config()?.esploraUrl}/address/${s().contractAddress}`}>
+                        <img src={lockOpenImage} class="me-2" />Open lockup address
+                    </a>
+                </Show>
             </>}</Show>
         </div>
     </>;
diff --git a/swap-frontend/src/SwapInService.ts b/swap-frontend/src/SwapInService.ts
index 8d901e36..64ac0600 100644
--- a/swap-frontend/src/SwapInService.ts
+++ b/swap-frontend/src/SwapInService.ts
@@ -1,5 +1,6 @@
 import { Psbt, Transaction } from 'bitcoinjs-lib';
 import {
+    Chain,
     FrontendConfiguration,
     GetSwapInResponse,
     getSwapInResponseSchema,
@@ -11,6 +12,8 @@ import {
 import { LocalSwapStorageService, PersistedSwapIn } from './LocalSwapStorageService.js';
 import { ECPairAPI } from 'ecpair';
 import { applicationContext } from './ApplicationContext.js';
+import * as liquid from 'liquidjs-lib';
+import * as ecc from 'tiny-secp256k1';
 
 export class SwapInService {
 
@@ -30,20 +33,57 @@ export class SwapInService {
         if (swap.status !== 'CONTRACT_EXPIRED') {
             throw new Error(`invalid state ${swap.status}`);
         }
-        const psbt = await this.getRefundPsbt(swap.swapId, address);
-        if (!this.isValidRefundTx(psbt, address)) {
-            throw new Error('Error building refund transactions');
+        let tx: Transaction | liquid.Transaction | null = null;
+        const psbtHex = await this.getRefundPsbtHex(swap.swapId, address);
+        if (swap.chain === 'BITCOIN') {
+            const psbt = Psbt.fromBase64(psbtHex, { network });
+            if (!this.isValidRefundTx(psbt, address)) {
+                throw new Error('Error building refund transactions');
+            }
+            signContractSpend({
+                psbt,
+                network,
+                key: this.ECPair.fromPrivateKey(refundPrivateKey),
+                preImage: Buffer.alloc(0),
+            });
+            if (psbt.getFeeRate() > 1000) {
+                throw new Error(`fee rate too high ${psbt.getFeeRate()}`);
+            }
+            tx = psbt.extractTransaction();
+        } else if (swap.chain === 'LIQUID') {
+            const pset = liquid.Pset.fromBase64(psbtHex);
+            if (!this.isValidLiquidRefundTx(pset, address)) {
+                throw new Error('Error building refund transactions');
+            }
+            const inputIndex = 0;
+            const input = pset.inputs[inputIndex];
+            const sighashType = liquid.Transaction.SIGHASH_ALL;
+            const signature = liquid.script.signature.encode(
+                this.ECPair.fromPrivateKey(refundPrivateKey).sign(pset.getInputPreimage(inputIndex, sighashType)),
+                sighashType,
+            );
+            const signer = new liquid.Signer(pset);
+            signer.addSignature(
+                inputIndex,
+                {
+                    partialSig: {
+                        pubkey: this.ECPair.fromPrivateKey(refundPrivateKey).publicKey,
+                        signature,
+                    },
+                },
+                liquid.Pset.ECDSASigValidator(ecc),
+            );
+            const finalizer = new liquid.Finalizer(pset);
+            const stack = [signature, Buffer.from(''), input.witnessScript!];
+            finalizer.finalizeInput(inputIndex, () => {
+                return {finalScriptWitness: liquid.witnessStackToScriptWitness(stack)};
+            });
+            tx = liquid.Extractor.extract(pset);
+            console.log(tx.toHex());
         }
-        signContractSpend({
-            psbt,
-            network,
-            key: this.ECPair.fromPrivateKey(refundPrivateKey),
-            preImage: Buffer.alloc(0),
-        });
-        if (psbt.getFeeRate() > 1000) {
-            throw new Error(`fee rate too high ${psbt.getFeeRate()}`);
+        if (tx == null) {
+            throw new Error('There was an error extracting the transaction');
         }
-        const tx = psbt.extractTransaction();
         await this.publishRefundTx(swap.swapId, tx);
     }
 
@@ -58,6 +98,14 @@ export class SwapInService {
         return true;
     }
 
+    isValidLiquidRefundTx(pset: liquid.Pset, address: string): boolean {
+        const outs = pset.outputs;
+        if (outs.length !== 2) { // In liquid the fee output is also included
+            return false;
+        }
+        return true;
+    }
+
     async getSwap(id: string): Promise<PersistedSwapIn> {
         const resp = await fetch(`/api/swap/in/${id}`);
         if (resp.status >= 300) {
@@ -74,9 +122,9 @@ export class SwapInService {
         };
     }
 
-    async createSwap(invoice: string): Promise<PersistedSwapIn> {
+    async createSwap(invoice: string, chain: Chain): Promise<PersistedSwapIn> {
         const refundKey = applicationContext.ECPair.makeRandom();
-        const swap = await this.postSwap(invoice, refundKey.publicKey);
+        const swap = await this.postSwap(invoice, refundKey.publicKey, chain);
         const localSwap: PersistedSwapIn = {
             type: 'in',
             ...swap,
@@ -86,19 +134,17 @@ export class SwapInService {
         return localSwap;
     }
 
-    private async getRefundPsbt(swapId: string, address: string): Promise<Psbt> {
-        const network = (await this.config).bitcoinNetwork;
+    private async getRefundPsbtHex(swapId: string, address: string): Promise<string> {
         const resp = await fetch(`/api/swap/in/${swapId}/refund-psbt?` + new URLSearchParams({
             address,
         }));
         if (resp.status >= 300) {
             throw new Error(`Unknown error getting refund psbt. ${await resp.text()}`);
         }
-        const psbt = Psbt.fromBase64(psbtResponseSchema.parse(await resp.json()).psbt, { network });
-        return psbt;
+        return psbtResponseSchema.parse(await resp.json()).psbt;
     }
 
-    private async publishRefundTx(swapId: string, tx: Transaction): Promise<void> {
+    private async publishRefundTx(swapId: string, tx: Transaction | liquid.Transaction): Promise<void> {
         const resp = await fetch(`/api/swap/in/${swapId}/refund-tx`, {
             method: 'POST',
             body: JSON.stringify({
@@ -113,13 +159,13 @@ export class SwapInService {
         }
     }
 
-    private async postSwap(invoice: string, refundPublicKey: Buffer): Promise<GetSwapInResponse> {
+    private async postSwap(invoice: string, refundPublicKey: Buffer, chain: Chain): Promise<GetSwapInResponse> {
         const resp = await fetch('/api/swap/in', {
             method: 'POST',
             body: JSON.stringify({
                 invoice,
                 refundPublicKey: refundPublicKey.toString('hex'),
-                chain: 'BITCOIN',
+                chain,
             } satisfies SwapInRequest),
             headers: {
                 'content-type': 'application/json',
diff --git a/swap-frontend/src/SwapOutService.ts b/swap-frontend/src/SwapOutService.ts
index 1479c09d..0ce1b10d 100644
--- a/swap-frontend/src/SwapOutService.ts
+++ b/swap-frontend/src/SwapOutService.ts
@@ -142,7 +142,7 @@ export class SwapOutService {
         }
     }
 
-    private async postSwap(amount: number, claimPubKey: Buffer, preImageHash: Buffer, chain: 'BITCOIN' | 'LIQUID'): Promise<GetSwapOutResponse> {
+    private async postSwap(amount: number, claimPubKey: Buffer, preImageHash: Buffer, chain: Chain): Promise<GetSwapOutResponse> {
         const resp = await fetch('/api/swap/out', {
             method: 'POST',
             body: JSON.stringify({
diff --git a/swap-frontend/src/controllers/AssetController.ts b/swap-frontend/src/controllers/AssetController.ts
index bd152d6e..9169edca 100644
--- a/swap-frontend/src/controllers/AssetController.ts
+++ b/swap-frontend/src/controllers/AssetController.ts
@@ -27,7 +27,7 @@ export const assetsConfiguration = [
         'name': 'ON_CHAIN_LIQUID',
         'displayName': 'Liquid',
         'icon': '/liquid-logo.svg',
-        'available': false,
+        'available': true,
         'restrictedAssets': [
             'ON_CHAIN_BITCOIN',
         ],


Reviews:
user: login: amanzag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T17:35:41Z
user: login: amanzag
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T17:35:59Z
user: login: alejandroacho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T08:25:15Z
user: login: alejandroacho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T08:25:19Z
user: login: amanzag
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:31:39Z

Review comments:
diff_hunk: @@ -74,9 +74,9 @@ export class SwapInService {
         };
     }
 
-    async createSwap(invoice: string): Promise<PersistedSwapIn> {
+    async createSwap(invoice: string, chain: 'BITCOIN' | 'LIQUID'): Promise<PersistedSwapIn> {
path: swap-frontend/src/SwapInService.ts
user: login: amanzag
body: ```suggestion
    async createSwap(invoice: string, chain: Chain): Promise<PersistedSwapIn> {
```
created_at: 2025-05-05T17:35:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,13 +113,13 @@ export class SwapInService {
         }
     }
 
-    private async postSwap(invoice: string, refundPublicKey: Buffer): Promise<GetSwapInResponse> {
+    private async postSwap(invoice: string, refundPublicKey: Buffer, chain: 'BITCOIN' | 'LIQUID'): Promise<GetSwapInResponse> {
path: swap-frontend/src/SwapInService.ts
user: login: amanzag
body: ```suggestion
    private async postSwap(invoice: string, refundPublicKey: Buffer, chain: Chain): Promise<GetSwapInResponse> {
```
created_at: 2025-05-05T17:35:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -74,9 +74,9 @@ export class SwapInService {
         };
     }
 
-    async createSwap(invoice: string): Promise<PersistedSwapIn> {
+    async createSwap(invoice: string, chain: 'BITCOIN' | 'LIQUID'): Promise<PersistedSwapIn> {
path: swap-frontend/src/SwapInService.ts
user: login: alejandroacho
body: Done
created_at: 2025-05-09T08:25:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,13 +113,13 @@ export class SwapInService {
         }
     }
 
-    private async postSwap(invoice: string, refundPublicKey: Buffer): Promise<GetSwapInResponse> {
+    private async postSwap(invoice: string, refundPublicKey: Buffer, chain: 'BITCOIN' | 'LIQUID'): Promise<GetSwapInResponse> {
path: swap-frontend/src/SwapInService.ts
user: login: alejandroacho
body: Done
created_at: 2025-05-09T08:25:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: amanzag
created_at: 2025-05-05T17:36:38Z
body: This is just the happy flow, right? I mean, unless I'm missing something, refunds won't work.
author_association: CONTRIBUTOR
user: login: amanzag
created_at: 2025-05-05T17:42:37Z
body: Some things that I think need fixing:
1. The bip21 link and qr code
2. The "Open lockup address" link (needs to point to mempool's liquid instance, I guess)
3. "Enter bitcoin address to receive refund" -> "Enter liquid address to receive refund"
author_association: CONTRIBUTOR
user: login: amanzag
created_at: 2025-05-06T13:58:26Z
body: Something else I just remembered: we need to add the chain to the history page.

And migrate any existing local data to add the new fields (in LocalSwapStorageService)
author_association: CONTRIBUTOR
user: login: alejandroacho
created_at: 2025-05-09T08:25:45Z
body: > This is just the happy flow, right? I mean, unless I'm missing something, refunds won't work.

Now it has the refund path too. 
author_association: CONTRIBUTOR
user: login: alejandroacho
created_at: 2025-05-09T10:37:20Z
body: > Some things that I think need fixing:
> 
> 1. The bip21 link and qr code
> 2. The "Open lockup address" link (needs to point to mempool's liquid instance, I guess)
> 3. "Enter bitcoin address to receive refund" -> "Enter liquid address to receive refund"

1. Implemented according the liquid community: https://github.com/ElementsProject/elements/issues/805
2. Done. We currently use Esplora on localhost, but maybe we could implement a way to use liquid mempool (at least on production, in order to keep coherence): https://liquid.network/es/
3. Done
author_association: CONTRIBUTOR
user: login: alejandroacho
created_at: 2025-05-09T10:39:04Z
body: > Something else I just remembered: we need to add the chain to the history page.
> 
> And migrate any existing local data to add the new fields (in LocalSwapStorageService)

I don't fully understand you. We currently display the correct logos in the history page:

![CleanShot 2025-05-09 at 12 38 05@2x](https://github.com/user-attachments/assets/46efa100-9152-4a2d-aaa9-4fb0b69f5e38)

Do we need anything else?
author_association: CONTRIBUTOR
user: login: amanzag
created_at: 2025-05-12T07:08:26Z
body: > We currently display the correct logos in the history page:

Ah, I was thinking we could also display the chain name as a new column, but the logo is probably enough.

Let me also explain this.
>migrate any existing local data to add the new fields (in LocalSwapStorageService)

Clients with any existing swaps in their browsers will see their history page fail when we deploy the new version because the data that's stored in the browser has some missing fields (e.g. chain).
We can use the migration mechanism of the local storage to add this field to any existing objects.


author_association: CONTRIBUTOR
user: login: alejandroacho
created_at: 2025-05-12T11:50:50Z
body: > > We currently display the correct logos in the history page:
> 
> Ah, I was thinking we could also display the chain name as a new column, but the logo is probably enough.
> 
> Let me also explain this.
> 
> > migrate any existing local data to add the new fields (in LocalSwapStorageService)
> 
> Clients with any existing swaps in their browsers will see their history page fail when we deploy the new version because the data that's stored in the browser has some missing fields (e.g. chain). We can use the migration mechanism of the local storage to add this field to any existing objects.

Done
author_association: CONTRIBUTOR