Skip to content

Commit

Permalink
fixup! feat: enable blockfrost input resolver in Lace
Browse files Browse the repository at this point in the history
  • Loading branch information
AngelCastilloB committed Jan 14, 2025
1 parent 0452ea7 commit c5eef30
Show file tree
Hide file tree
Showing 5 changed files with 13 additions and 230 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export const getProviders = async (chainName: Wallet.ChainName): Promise<Wallet.
useBlockfrostRewardsProvider: isExperimentEnabled(ExperimentName.BLOCKFROST_REWARDS_PROVIDER),
useBlockfrostTxSubmitProvider: isExperimentEnabled(ExperimentName.BLOCKFROST_TX_SUBMIT_PROVIDER),
useBlockfrostUtxoProvider: isExperimentEnabled(ExperimentName.BLOCKFROST_UTXO_PROVIDER),
useBlockfrostAddressDiscovery: isExperimentEnabled(ExperimentName.BLOCKFROST_ADDRESS_DISCOVERY)
useBlockfrostAddressDiscovery: isExperimentEnabled(ExperimentName.BLOCKFROST_ADDRESS_DISCOVERY),
useBlockfrostInputResolver: isExperimentEnabled(ExperimentName.BLOCKFROST_INPUT_RESOLVER)
}
});
};
Expand Down
34 changes: 2 additions & 32 deletions apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable unicorn/no-null */
import { runtime, storage as webStorage } from 'webextension-polyfill';
import { of, combineLatest, map, EMPTY, BehaviorSubject, Observable, from, firstValueFrom, defaultIfEmpty } from 'rxjs';
import { getProviders, rateLimiter } from './config';
import { getProviders } from './config';
import { DEFAULT_POLLING_CONFIG, createPersonalWallet, storage, createSharedWallet } from '@cardano-sdk/wallet';
import { BlockfrostClient, handleHttpProvider } from '@cardano-sdk/cardano-services-client';
import { handleHttpProvider } from '@cardano-sdk/cardano-services-client';
import { Cardano, HandleProvider } from '@cardano-sdk/core';
import {
AnyWallet,
Expand Down Expand Up @@ -36,7 +36,6 @@ import { ExtensionDocumentStore } from './storage/extension-document-store';
import { ExtensionBlobKeyValueStore } from './storage/extension-blob-key-value-store';
import { ExtensionBlobCollectionStore } from './storage/extension-blob-collection-store';
import { migrateCollectionStore, migrateWalletStores, shouldAttemptWalletStoresMigration } from './storage/migrations';
import { config } from '@src/config';

if (typeof window !== 'undefined') {
throw new TypeError('This module should only be imported in service worker');
Expand Down Expand Up @@ -130,25 +129,6 @@ const walletFactory: WalletFactory<Wallet.WalletMetadata, Wallet.AccountMetadata
}

const featureFlags = await getFeatureFlags(chainId.networkMagic);
const useBlockfrostInputResolver = isExperimentEnabled(featureFlags, ExperimentName.BLOCKFROST_INPUT_RESOLVER);

let inputResolver;

if (useBlockfrostInputResolver) {
const { BLOCKFROST_CONFIGS } = config();

const blockfrostClient = new BlockfrostClient(
{
...BLOCKFROST_CONFIGS[chainName],
apiVersion: 'v0'
},
{
rateLimiter
}
);

inputResolver = new Wallet.BlockfrostInputResolver(blockfrostClient, logger);
}

if (wallet.type === WalletType.Script) {
const stakingScript = wallet.stakingScript as SharedWalletScriptKind;
Expand All @@ -158,7 +138,6 @@ const walletFactory: WalletFactory<Wallet.WalletMetadata, Wallet.AccountMetadata
{ name: wallet.metadata.name },
{
...providers,
inputResolver,
logger,
paymentScript,
stakingScript,
Expand All @@ -174,10 +153,6 @@ const walletFactory: WalletFactory<Wallet.WalletMetadata, Wallet.AccountMetadata
}
);

if (inputResolver) {
inputResolver.setContext(sharedWallet);
}

// Caches current wallet providers.
providers = { ...providers, inputResolver: { resolveInput: sharedWallet.util.resolveInput } };
currentWalletProviders$.next(providers);
Expand Down Expand Up @@ -212,7 +187,6 @@ const walletFactory: WalletFactory<Wallet.WalletMetadata, Wallet.AccountMetadata
{
logger,
...providers,
inputResolver,
stores,
handleProvider: supportsHandleResolver
? handleHttpProvider({
Expand All @@ -226,10 +200,6 @@ const walletFactory: WalletFactory<Wallet.WalletMetadata, Wallet.AccountMetadata
}
);

if (inputResolver) {
inputResolver.setContext(personalWallet);
}

// Caches current wallet providers.
providers = { ...providers, inputResolver: { resolveInput: personalWallet.util.resolveInput } };
currentWalletProviders$.next(providers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import { BlockfrostInputResolver } from '../blockfrost-input-resolver';
import { Cardano } from '@cardano-sdk/core';
import { BlockfrostClient, BlockfrostError, BlockfrostToCore } from '@cardano-sdk/cardano-services-client';
import { Logger } from 'ts-log';
import { InputResolverContext } from '@cardano-sdk/wallet';
import { of } from 'rxjs';
import { WitnessedTx } from '@cardano-sdk/key-management';

jest.mock('@cardano-sdk/cardano-services-client');

Expand Down Expand Up @@ -108,117 +105,4 @@ describe('BlockfrostInputResolver', () => {
expect(error_.status).toEqual(500);
}
});

describe('context resolution', () => {
let contextMock: InputResolverContext;

beforeEach(() => {
contextMock = {
transactions: {
history$: of([]),
outgoing: {
signed$: of([])
}
},
utxo: {
available$: of([])
}
};

resolver.setContext(contextMock);
});

it('should resolve input from context.utxo.available$ if present', async () => {
const targetTxIn: Cardano.HydratedTxIn = {
address: 'addr...' as Cardano.PaymentAddress,
txId: 'ctxTxId' as Cardano.TransactionId,
index: 1
};
const targetTxOut: Cardano.TxOut = {
address: 'ctxAddr' as Cardano.PaymentAddress,
value: { coins: BigInt(999) }
};

contextMock.utxo.available$ = of([
[targetTxIn, targetTxOut],
[
{ address: 'addr...' as Cardano.PaymentAddress, txId: 'otherTxId' as Cardano.TransactionId, index: 0 },
{ address: 'otherAddr' as Cardano.PaymentAddress, value: { coins: BigInt(123) } }
]
]);

const result = await resolver.resolveInput(targetTxIn);
expect(result).toEqual(targetTxOut);
expect(clientMock.request).not.toHaveBeenCalled();
});

it('should resolve input from context.transactions.outgoing.signed$ if present', async () => {
const targetTxIn: Cardano.TxIn = { txId: 'signedTxId' as Cardano.TransactionId, index: 0 };
const targetTxOut: Cardano.TxOut = {
address: 'signedTxAddr' as Cardano.PaymentAddress,
value: { coins: BigInt(1234) }
};

const witnessedTx: WitnessedTx = {
tx: {
id: 'signedTxId' as Cardano.TransactionId,
body: {
inputs: [],
outputs: [targetTxOut]
} as unknown as Cardano.TxBody
} as unknown as Cardano.Tx
} as unknown as WitnessedTx;

contextMock.transactions.outgoing.signed$ = of([witnessedTx]);

const result = await resolver.resolveInput(targetTxIn);
expect(result).toEqual(targetTxOut);
expect(clientMock.request).not.toHaveBeenCalled();
});

it('should resolve input from context.transactions.history$ if present', async () => {
const targetTxIn: Cardano.TxIn = { txId: 'historyTxId' as Cardano.TransactionId, index: 1 };
const targetTxOut: Cardano.TxOut = {
address: 'historyTxAddr' as Cardano.PaymentAddress,
value: { coins: BigInt(777) }
};

contextMock.transactions.history$ = of([
{
id: 'historyTxId' as Cardano.TransactionId,
body: {
inputs: [],
outputs: [
{ address: 'addr...' as Cardano.PaymentAddress, value: { coins: BigInt(0) } },
targetTxOut
] as Cardano.TxOut[]
} as unknown as Cardano.TxBody
} as unknown as Cardano.HydratedTx
]);

const result = await resolver.resolveInput(targetTxIn);
expect(result).toEqual(targetTxOut);
expect(clientMock.request).not.toHaveBeenCalled();
});

it('should fallback to Blockfrost if not found in context', async () => {
const txIn: Cardano.TxIn = { txId: 'fallbackTxId' as Cardano.TransactionId, index: 0 };
const responseMock = {
outputs: [{ output_index: 0, address: 'fetchedAddr', amount: [{ unit: 'lovelace', quantity: '3000' }] }]
};
clientMock.request.mockResolvedValue(responseMock);

jest.spyOn(BlockfrostToCore, 'txOut').mockReturnValue({
address: 'fetchedAddr' as Cardano.PaymentAddress,
value: { coins: BigInt(3000) }
} as Cardano.TxOut);

const result = await resolver.resolveInput(txIn);
expect(result).toEqual({
address: 'fetchedAddr',
value: { coins: BigInt(3000) }
});
expect(clientMock.request).toHaveBeenCalledWith('txs/fallbackTxId/utxos');
});
});
});
80 changes: 1 addition & 79 deletions packages/cardano/src/wallet/lib/blockfrost-input-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import { Cardano } from '@cardano-sdk/core';
import { BlockfrostClient, BlockfrostError, BlockfrostToCore } from '@cardano-sdk/cardano-services-client';
import { Logger } from 'ts-log';
import { Responses } from '@blockfrost/blockfrost-js';
import { InputResolverContext, txInEquals } from '@cardano-sdk/wallet';
import { firstValueFrom } from 'rxjs';
import { WitnessedTx } from '@cardano-sdk/key-management';

const NOT_FOUND_STATUS = 404;

Expand All @@ -24,7 +21,6 @@ export class BlockfrostInputResolver implements Cardano.InputResolver {
readonly #logger: Logger;
readonly #client: BlockfrostClient;
readonly #txCache = new Map<string, Cardano.TxOut>();
#context: InputResolverContext | undefined;

/**
* Constructs a new BlockfrostInputResolver.
Expand Down Expand Up @@ -53,10 +49,7 @@ export class BlockfrostInputResolver implements Cardano.InputResolver {
return this.#txCache.get(txInToId(input))!;
}

let resolved = await this.resolveFromContext(input);
if (resolved) return resolved;

resolved = this.resolveFromHints(input, options);
const resolved = this.resolveFromHints(input, options);
if (resolved) return resolved;

const out = await this.fetchAndCacheTxOut(input);
Expand All @@ -65,24 +58,6 @@ export class BlockfrostInputResolver implements Cardano.InputResolver {
return out;
}

/**
* Sets the input resolution context (e.g., references to transaction history, available UTXOs, outgoing signed TXs).
*
* @param context - An instance of `InputResolverContext` providing data for resolution.
*/
public setContext(context: InputResolverContext): void {
this.#context = context;
}

/**
* Retrieves the current input resolution context, if any.
*
* @returns The `InputResolverContext` instance, or `undefined` if not set.
*/
public getContext(): InputResolverContext | undefined {
return this.#context;
}

/**
* Attempts to resolve the provided input from the hints provided in the resolution options.
* @param input - The transaction input to resolve.
Expand Down Expand Up @@ -153,57 +128,4 @@ export class BlockfrostInputResolver implements Cardano.InputResolver {
this.#logger.error(`Failed to resolve input ${txIn.txId}#${txIn.index}`);
return null;
}

/**
* Attempts to resolve the provided input from the in-memory context (if available).
*
* The context includes:
* - Transaction history
* - Currently available UTXOs
* - Outgoing signed transactions that are not yet broadcast but may contain UTXOs
*
* @private
* @param input - The transaction input to resolve.
* @returns A promise that resolves to the corresponding `Cardano.TxOut` if found in context, or `null` otherwise.
*/
private async resolveFromContext(input: Cardano.TxIn): Promise<Cardano.TxOut | null> {
if (!this.#context) return null;

const txHistory = await firstValueFrom(this.#context.transactions.history$);
const utxoAvailable = await firstValueFrom(this.#context.utxo.available$, { defaultValue: [] });
const signedTransactions = await firstValueFrom(this.#context.transactions.outgoing.signed$, {
defaultValue: new Array<WitnessedTx>()
});
const utxoFromSigned = signedTransactions.flatMap(({ tx: signedTx }, signedTxIndex) =>
signedTx.body.outputs
.filter((_, outputIndex) => {
const alreadyConsumed = signedTransactions.some(
({ tx: { body } }, i) =>
signedTxIndex !== i &&
body.inputs.some((consumedInput) => txInEquals(consumedInput, { index: outputIndex, txId: signedTx.id }))
);

return !alreadyConsumed;
})
.map((txOut): Cardano.Utxo => {
const txIn: Cardano.HydratedTxIn = {
address: txOut.address,
index: signedTx.body.outputs.indexOf(txOut),
txId: signedTx.id
};
return [txIn, txOut];
})
);
const availableUtxo = [...utxoAvailable, ...utxoFromSigned].find(([txIn]) => txInEquals(txIn, input));

if (availableUtxo) return availableUtxo[1];

const historyTx = txHistory.findLast((entry) => entry.id === input.txId);

if (historyTx && historyTx.body.outputs.length > input.index) {
return historyTx.body.outputs[input.index];
}

return null;
}
}
10 changes: 8 additions & 2 deletions packages/cardano/src/wallet/lib/providers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-new, complexity, sonarjs/cognitive-complexity */
import { WalletProvidersDependencies } from '@src/wallet';
import { BlockfrostInputResolver, WalletProvidersDependencies } from '@src/wallet';
import { AxiosAdapter } from 'axios';
import { Logger } from 'ts-log';
import {
Expand Down Expand Up @@ -78,7 +78,7 @@ export type RateLimiterConfig = {
increaseAmount: number;
};

export interface ProvidersConfig {
interface ProvidersConfig {
axiosAdapter?: AxiosAdapter;
env: {
baseCardanoServicesUrl: string;
Expand All @@ -96,6 +96,7 @@ export interface ProvidersConfig {
useBlockfrostTxSubmitProvider?: boolean;
useBlockfrostUtxoProvider?: boolean;
useBlockfrostAddressDiscovery?: boolean;
useBlockfrostInputResolver?: boolean;
};
}

Expand All @@ -118,6 +119,7 @@ export const createProviders = ({
useBlockfrostUtxoProvider,
useDrepProviderOverrideActiveStatus,
useBlockfrostAddressDiscovery,
useBlockfrostInputResolver,
useWebSocket
}
}: ProvidersConfig): WalletProvidersDependencies => {
Expand Down Expand Up @@ -150,6 +152,8 @@ export const createProviders = ({
? new BlockfrostAddressDiscovery(blockfrostClient, logger)
: new HDSequentialDiscovery(chainHistoryProvider, DEFAULT_LOOK_AHEAD_SEARCH);

const inputResolver = useBlockfrostInputResolver ? new BlockfrostInputResolver(blockfrostClient, logger) : undefined;

// Temporary proxy for drepProvider to overwrite the 'active' property to always be true
const drepProviderOverrideActiveStatus = new Proxy(drepProvider, {
get(target, property, receiver) {
Expand Down Expand Up @@ -200,6 +204,7 @@ export const createProviders = ({
rewardsProvider,
wsProvider,
addressDiscovery,
inputResolver,
drepProvider: useDrepProviderOverrideActiveStatus ? drepProviderOverrideActiveStatus : drepProvider
};
}
Expand All @@ -217,6 +222,7 @@ export const createProviders = ({
chainHistoryProvider,
rewardsProvider,
addressDiscovery,
inputResolver,
drepProvider: useDrepProviderOverrideActiveStatus ? drepProviderOverrideActiveStatus : drepProvider
};
};
Expand Down

0 comments on commit c5eef30

Please sign in to comment.