Skip to content

Commit

Permalink
refactor: migrate slip77
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Feb 18, 2025
1 parent a825115 commit eca70d1
Show file tree
Hide file tree
Showing 19 changed files with 603 additions and 273 deletions.
65 changes: 53 additions & 12 deletions lib/Core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
reverseBuffer,
} from './Utils';
import { IChainClient } from './chain/ChainClient';
import { SomeTransaction } from './chain/ZmqClient';
import {
CurrencyType,
SwapType,
Expand All @@ -61,7 +62,7 @@ import ChainSwapData from './db/models/ChainSwapData';
import Swap from './db/models/Swap';
import SwapOutputType from './swap/SwapOutputType';
import Wallet from './wallet/Wallet';
import WalletLiquid from './wallet/WalletLiquid';
import WalletLiquid, { Slip77s } from './wallet/WalletLiquid';
import { Currency } from './wallet/WalletManager';

type UnblindedOutput = Omit<LiquidTxOutput, 'value'> & {
Expand Down Expand Up @@ -159,14 +160,24 @@ export const getOutputValue = (
return output.value as number;
}

const unblinded = unblindOutput(
wallet,
output as LiquidTxOutput,
(wallet as WalletLiquid).deriveBlindingKeyFromScript(output.script)
.privateKey!,
const walletKeys = (wallet as WalletLiquid).deriveBlindingKeyFromScript(
output.script,
);

return unblinded.isLbtc ? unblinded.value : 0;
for (const key of [walletKeys.new, walletKeys.legacy]
.map((k) => k.privateKey)
.filter((k) => k !== undefined)) {
try {
const unblinded = unblindOutput(wallet, output as LiquidTxOutput, key);

return unblinded.isLbtc ? unblinded.value : 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
/* empty */
}
}

return 0;
};

export const constructClaimDetails = (
Expand Down Expand Up @@ -230,7 +241,8 @@ export const constructClaimTransaction = (
claimDetails: ClaimDetails[] | LiquidClaimDetails[],
destinationAddress: string,
feePerVbyte: number,
) => {
blindingKeyType: keyof Slip77s = 'new',
): SomeTransaction => {
const span = Tracing.tracer.startSpan('constructClaimTransaction', {
kind: SpanKind.INTERNAL,
attributes: {
Expand All @@ -257,6 +269,7 @@ export const constructClaimTransaction = (
const liquidDetails = populateBlindingKeys(
walletLiquid,
claimDetails as LiquidClaimDetails[],
blindingKeyType,
);
const decodedAddress = liquidAddress.fromConfidential(destinationAddress);

Expand All @@ -274,6 +287,18 @@ export const constructClaimTransaction = (
walletLiquid.supportsDiscountCT,
);
});
} catch (e) {
if (blindingKeyType === 'new') {
return constructClaimTransaction(
wallet,
claimDetails,
destinationAddress,
feePerVbyte,
'legacy',
);
}

throw e;
} finally {
span.end();
}
Expand All @@ -285,7 +310,8 @@ export const constructRefundTransaction = (
destinationAddress: string,
timeoutBlockHeight: number,
feePerVbyte: number,
) => {
blindingKeyType: keyof Slip77s = 'new',
): SomeTransaction => {
const span = Tracing.tracer.startSpan('constructRefundTransaction', {
kind: SpanKind.INTERNAL,
attributes: {
Expand Down Expand Up @@ -313,6 +339,7 @@ export const constructRefundTransaction = (
const liquidDetails = populateBlindingKeys(
walletLiquid,
refundDetails as LiquidRefundDetails[],
blindingKeyType,
);
const decodedAddress = liquidAddress.fromConfidential(destinationAddress);

Expand All @@ -331,6 +358,19 @@ export const constructRefundTransaction = (
walletLiquid.supportsDiscountCT,
);
});
} catch (e) {
if (blindingKeyType === 'new') {
return constructRefundTransaction(
wallet,
refundDetails,
destinationAddress,
timeoutBlockHeight,
feePerVbyte,
'legacy',
);
}

throw e;
} finally {
span.end();
}
Expand Down Expand Up @@ -413,11 +453,12 @@ const populateBlindingKeys = <
>(
wallet: WalletLiquid,
utxos: T[],
blindingKeyType: keyof Slip77s,
): T[] => {
for (const utxo of utxos) {
utxo.blindingPrivateKey = wallet.deriveBlindingKeyFromScript(
utxo.script,
).privateKey!;
utxo.blindingPrivateKey = wallet.deriveBlindingKeyFromScript(utxo.script)[
blindingKeyType
].privateKey!;
}

return utxos;
Expand Down
11 changes: 7 additions & 4 deletions lib/service/ElementsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ class ElementsService {
return await Promise.all(
tx.outs.map(async (out) => {
if (out.rangeProof !== undefined && out.rangeProof.length > 0) {
const walletKeys = wallet.deriveBlindingKeyFromScript(out.script)!;
const keys = [
wallet.deriveBlindingKeyFromScript(out.script).privateKey!,
walletKeys.new.privateKey,
walletKeys.legacy.privateKey,
getHexBuffer(
await chainClient.dumpBlindingKey(
wallet.encodeAddress(out.script, false),
// Does not matter which one we take because we don't blind
wallet.encodeAddress(out.script, false).new,
),
),
];

for (const blindingKey of keys) {
for (const blindingKey of keys.filter((k) => k !== undefined)) {
try {
return unblindOutput(wallet, out, blindingKey);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -56,7 +59,7 @@ class ElementsService {

const { publicKey, privateKey } = wallet.deriveBlindingKeyFromScript(
wallet.decodeAddress(address),
);
).new;
return {
publicKey: publicKey!,
privateKey: privateKey!,
Expand Down
2 changes: 1 addition & 1 deletion lib/service/TransactionFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class TransactionFetcher {
? transaction.outs
// Filter Liquid fee outputs
.filter((out) => out.script.length > 0)
.map((out) => wallet.encodeAddress(out.script))
.flatMap((out) => Object.values(wallet.encodeAddress(out.script)))
// The wallet returns empty strings for addresses it cannot encode
.filter((addr) => addr !== '')
: [];
Expand Down
13 changes: 7 additions & 6 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,14 +416,14 @@ class SwapManager {
}
}

result.address = receivingCurrency.wallet.encodeAddress(outputScript);
result.address = receivingCurrency.wallet.encodeAddress(outputScript).new;
receivingCurrency.chainClient!.addOutputFilter(outputScript);

if (receivingCurrency.type === CurrencyType.Liquid) {
result.blindingKey = getHexString(
(
receivingCurrency.wallet as WalletLiquid
).deriveBlindingKeyFromScript(outputScript).privateKey!,
).deriveBlindingKeyFromScript(outputScript).new.privateKey!,
);
}

Expand Down Expand Up @@ -872,13 +872,14 @@ class SwapManager {
}
}

result.lockupAddress = sendingCurrency.wallet.encodeAddress(outputScript);
result.lockupAddress =
sendingCurrency.wallet.encodeAddress(outputScript).new;

if (sendingCurrency.type === CurrencyType.Liquid) {
result.blindingKey = getHexString(
(sendingCurrency.wallet as WalletLiquid).deriveBlindingKeyFromScript(
outputScript,
).privateKey!,
).new.privateKey!,
);
}

Expand Down Expand Up @@ -1066,13 +1067,13 @@ class SwapManager {
currency.chainClient!.addOutputFilter(outputScript);
}

res.lockupAddress = currency.wallet.encodeAddress(outputScript);
res.lockupAddress = currency.wallet.encodeAddress(outputScript).new;

if (currency.type === CurrencyType.Liquid) {
blindingKey = getHexString(
(currency.wallet as WalletLiquid).deriveBlindingKeyFromScript(
outputScript,
).privateKey!,
).new.privateKey!,
);
}
} else {
Expand Down
17 changes: 13 additions & 4 deletions lib/swap/UtxoNursery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ class UtxoNursery extends TypedEventEmitter<{
chainClient,
wallet,
);
if (prevAddresses.some(this.blocks.isBlocked)) {
if (
prevAddresses.flatMap((a) => Object.values(a)).some(this.blocks.isBlocked)
) {
chainClient.removeOutputFilter(swapOutput.script);
this.emit('chainSwap.lockup.failed', {
swap,
Expand Down Expand Up @@ -326,9 +328,14 @@ class UtxoNursery extends TypedEventEmitter<{
await this.lock.acquire(UtxoNursery.lockupLock, async () => {
for (let vout = 0; vout < transaction.outs.length; vout += 1) {
const output = transaction.outs[vout];
const address = wallet.encodeAddress(output.script);
const encoded = wallet.encodeAddress(output.script);

await Promise.all([checkSwap(address), checkChainSwap(address)]);
await Promise.all(
[...new Set([encoded.new, encoded.legacy])].flatMap((a) => [
checkSwap(a),
checkChainSwap(a),
]),
);
}
});
};
Expand Down Expand Up @@ -794,7 +801,9 @@ class UtxoNursery extends TypedEventEmitter<{
chainClient,
wallet,
);
if (prevAddresses.some(this.blocks.isBlocked)) {
if (
prevAddresses.flatMap((a) => Object.values(a)).some(this.blocks.isBlocked)
) {
this.emit('swap.lockup.failed', {
swap: updatedSwap,
reason: Errors.BLOCKED_ADDRESS().message,
Expand Down
16 changes: 13 additions & 3 deletions lib/wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Logger from '../Logger';
import { CurrencyType } from '../consts/Enums';
import KeyRepository from '../db/repositories/KeyRepository';
import Errors from './Errors';
import type { Slip77s } from './WalletLiquid';
import WalletProviderInterface, {
BalancerFetcher,
SentTransaction,
Expand Down Expand Up @@ -86,17 +87,26 @@ class Wallet implements BalancerFetcher {
*
* @param outputScript the output script to encode
*/
public encodeAddress = (outputScript: Buffer): string => {
public encodeAddress = (
outputScript: Buffer,
): Record<keyof Slip77s, string> => {
if (this.network === undefined) {
throw Errors.NOT_SUPPORTED_BY_WALLET(this.symbol, 'encodeAddress');
}

try {
return fromOutputScript(this.type, outputScript, this.network);
const address = fromOutputScript(this.type, outputScript, this.network);
return {
new: address,
legacy: address,
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// Ignore invalid addresses
return '';
return {
new: '',
legacy: '',
};
}
};

Expand Down
57 changes: 46 additions & 11 deletions lib/wallet/WalletLiquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import { CurrencyType } from '../consts/Enums';
import Wallet from './Wallet';
import WalletProviderInterface from './providers/WalletProviderInterface';

export type Slip77s = {
new: Slip77Interface;
legacy: Slip77Interface;
};

class WalletLiquid extends Wallet {
constructor(
logger: Logger,
walletProvider: WalletProviderInterface,
private readonly slip77: Slip77Interface,
private readonly slip77s: Slip77s,
network: Network,
) {
super(logger, CurrencyType.Liquid, walletProvider, network);
Expand All @@ -27,33 +32,63 @@ class WalletLiquid extends Wallet {
);
}

public deriveBlindingKeyFromScript = (outputScript: Buffer) => {
return this.slip77.derive(outputScript);
public deriveBlindingKeyFromScript = (
outputScript: Buffer,
): Record<keyof Slip77s, Slip77Interface> => {
return {
new: this.slip77s.new.derive(outputScript),
legacy: this.slip77s.legacy.derive(outputScript),
};
};

public override encodeAddress = (
outputScript: Buffer,
shouldBlind = true,
): string => {
): Record<keyof Slip77s, string> => {
try {
// Fee output of Liquid
if (outputScript.length == 0) {
return '';
return {
new: '',
legacy: '',
};
}

if (!shouldBlind) {
const res = this.getPaymentFunc(outputScript)({
output: outputScript,
network: this.network as networks.Network,
});
return {
new: res.address!,
legacy: res.address!,
};
}

const res = this.getPaymentFunc(outputScript)({
const walletKeys = this.deriveBlindingKeyFromScript(outputScript);

const resNew = this.getPaymentFunc(outputScript)({
output: outputScript,
network: this.network as networks.Network,
blindkey: walletKeys.new.publicKey,
});
const resLegacy = this.getPaymentFunc(outputScript)({
output: outputScript,
network: this.network as networks.Network,
blindkey: shouldBlind
? this.deriveBlindingKeyFromScript(outputScript).publicKey!
: undefined,
blindkey: walletKeys.legacy.publicKey,
});

return shouldBlind ? res.confidentialAddress! : res.address!;
return {
new: resNew.confidentialAddress!,
legacy: resLegacy.confidentialAddress!,
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// Ignore invalid addresses
return '';
return {
new: '',
legacy: '',
};
}
};

Expand Down
Loading

0 comments on commit eca70d1

Please sign in to comment.