Skip to content

feat(wallet): remove auto-collateral management logic [LW-12322] #1595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 3 additions & 84 deletions packages/wallet/src/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ export type Cip30WalletDependencies = {
export enum Cip30ConfirmationCallbackType {
SignData = 'sign_data',
SignTx = 'sign_tx',
SubmitTx = 'submit_tx',
GetCollateral = 'get_collateral'
SubmitTx = 'submit_tx'
}

export type SignDataCallbackParams = {
Expand All @@ -59,18 +58,6 @@ export type SubmitTxCallbackParams = {
data: Cardano.Tx;
};

// Optional callback
export type GetCollateralCallbackParams = {
sender: MessageSender;
type: Cip30ConfirmationCallbackType.GetCollateral;
data: {
amount: Cardano.Lovelace;
utxos: Cardano.Utxo[];
};
};

type GetCollateralCallback = (args: GetCollateralCallbackParams) => Promise<Cardano.Utxo[]>;

export type SignConfirmationOk = { cancel$: Observable<void> };
export type SignConfirmationResult = SignConfirmationOk | false;

Expand All @@ -81,7 +68,6 @@ export type CallbackConfirmation = {
signData: (args: SignDataCallbackParams) => Promise<SignConfirmationResult>;
signTx: (args: SignTxCallbackParams) => Promise<SignConfirmationResult>;
submitTx: (args: SubmitTxCallbackParams) => Promise<boolean>;
getCollateral?: GetCollateralCallback;
};

const firstValueFromTimed = <T>(observable$: Observable<T>, timeoutAfter: Milliseconds) =>
Expand Down Expand Up @@ -194,9 +180,6 @@ const selectUtxo = async (wallet: ObservableWallet, filterAmount: Cardano.Value,
? dumbSelection(await firstValueFrom(wallet.utxo.available$), filterAmount)
: await walletSelection(filterAmount, wallet);

/** Returns an array of UTxOs that do not contain assets */
const getUtxosWithoutAssets = (utxos: Cardano.Utxo[]): Cardano.Utxo[] => utxos.filter((utxo) => !utxo[1].value.assets);

const getFilterAsBigNum = (amount: Cbor): bigint => {
const reader = new Serialization.CborReader(HexBlob(amount));

Expand Down Expand Up @@ -224,46 +207,6 @@ const getFilterAmount = (amount: Cbor): bigint => {
}
};

/**
* getCollateralCallback
*
* @param sender The sender of the request
* @param amount ADA collateral required in lovelaces
* @param availableUtxos available UTxOs
* @param callback Callback to execute to attempt setting new collateral
* @param logger The logger instance
* @returns Promise<Cbor[]> or null
*/
const getCollateralCallback = async (
sender: MessageSender,
amount: Cardano.Lovelace,
availableUtxos: Cardano.Utxo[],
callback: GetCollateralCallback,
logger: Logger
) => {
if (availableUtxos.length === 0) return null;
const availableUtxosWithoutAssets = getUtxosWithoutAssets(availableUtxos);
try {
// Send the amount and filtered available UTxOs to the callback
// Client can then choose to mark a UTxO set as unspendable
const newCollateral = await callback({
data: {
amount,
utxos: availableUtxosWithoutAssets
},
sender,
type: Cip30ConfirmationCallbackType.GetCollateral
});
return newCollateral.map((core) => Serialization.TransactionUnspentOutput.fromCore(core).toCbor());
} catch (error) {
logger.error(error);
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(APIErrorCode.InternalError, formatUnknownError(error));
}
};

const getSortedUtxos = async (observableUtxos: Observable<Cardano.Utxo[]>): Promise<Cardano.Utxo[]> => {
const utxos = await firstValueFrom(observableUtxos);
return utxos.sort(compareUtxos);
Expand Down Expand Up @@ -333,31 +276,19 @@ const baseCip30WalletApi = (
throw new ApiError(APIErrorCode.InternalError, formatUnknownError(error));
}
},
// eslint-disable-next-line max-statements, sonarjs/cognitive-complexity,complexity
getCollateral: async (
{ sender }: SenderContext,
_: SenderContext,
{ amount = new Serialization.Value(MAX_COLLATERAL_AMOUNT).toCbor() }: { amount?: Cbor } = {}
): Promise<
Cbor[] | null
// eslint-disable-next-line sonarjs/cognitive-complexity, max-statements
// eslint-disable-next-line sonarjs/cognitive-complexity
> => {
logger.debug('getting collateral');
const wallet = await firstValueFrom(wallet$);
await waitForWalletStateSettle(wallet);
let unspendables = await getSortedUtxos(wallet.utxo.unspendable$);
const available = await getSortedUtxos(wallet.utxo.available$);
// No available unspendable UTxO
if (unspendables.length === 0) {
if (available.length > 0 && !!confirmationCallback.getCollateral) {
// available UTxOs could be set as collateral based on user preference
return await getCollateralCallback(
sender,
getFilterAmount(amount),
available,
confirmationCallback.getCollateral,
logger
);
}
return null;
}

Expand All @@ -377,18 +308,6 @@ const baseCip30WalletApi = (
if (totalCoins >= filterAmount) break;
}
if (totalCoins < filterAmount) {
// if no collateral available by amount in unspendables, return callback if provided to set unspendables and return in the callback

if (available.length > 0 && !!confirmationCallback.getCollateral) {
return await getCollateralCallback(
sender,
filterAmount,
available,
confirmationCallback.getCollateral,
logger
);
}

throw new ApiError(APIErrorCode.Refused, 'not enough coins in configured collateral UTxOs');
}
unspendables = utxos;
Expand Down
154 changes: 4 additions & 150 deletions packages/wallet/test/integration/cip30mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { AddressType, Bip32Account, GroupedAddress, KeyRole, util } from '@cardano-sdk/key-management';
import { AssetId, mockProviders as mocks } from '@cardano-sdk/util-dev';
import { BaseWallet, ObservableWallet, cip30, createPersonalWallet } from '../../src';
import { CallbackConfirmation, GetCollateralCallbackParams } from '../../src/cip30';
import { CallbackConfirmation } from '../../src/cip30';
import {
Cardano,
OutsideOfValidityIntervalData,
Expand All @@ -45,14 +45,10 @@ const {
mockRewardsProvider,
mockTxSubmitProvider,
utxo: mockUtxo,
utxosWithLowCoins,
utxosWithLowCoinsAndMixedAssets,
sortedUtxosWithLowCoins,
impureUtxos
utxosWithLowCoins
} = mocks;

type TestProviders = Required<Pick<Providers, 'txSubmitProvider' | 'networkInfoProvider'>>;
const mockCollateralCallback = jest.fn().mockResolvedValue([mockUtxo[3]]);
const createMockGenericCallback = <T>(result: T) => jest.fn().mockResolvedValue(result);
const foreignTx = Serialization.TxCBOR(
'84a70081825820dce442e983f3f5cd5b2644bc57f749075390f1fbae9ab55bf454342959c885db00018182583900d161d64eef0eeb59f9124f520f8c8f3b717ed04198d54c8b17e604aea63c153fb3ea8a4ea4f165574ea91173756de0bf30222ca0e95a649a1a0082607b021a0016360509a1581cb77934706fa311b6568d1070c2d23f092324b35ad623aa571a0e3726a14e4d6573685f476966745f43617264200b5820d8175f3b1276a48939a6ccee220a7f81b6422167317ba3ff6325cba1fb6ccbe70d818258208d68748457cd0f1a8596f41fd2125a415315897d2da4a4b94335829cee7198ae001281825820dce442e983f3f5cd5b2644bc57f749075390f1fbae9ab55bf454342959c885db00a2068259016b590168010000333232323232323223223222253330083232533300d3010002132533300b3370e6eb4c034009200113371e0020122940dd718058008b180700099299980499b8748008c028dd50008a5eb7bdb1804dd5980718059baa001323300100132330010013756601e602060206020602060186ea8c03cc030dd50019129998070008a5eb7bdb1804c8c8c8c94ccc03ccdc8a45000021533300f3371e91010000210031005133013337606ea4008dd3000998030030019bab3010003375c601c0046024004602000244a66601a002298103d87a8000132323232533300e337220140042a66601c66e3c0280084cdd2a4000660246e980052f5c02980103d87a80001330060060033756601e0066eb8c034008c044008c03c00452613656375c0026eb80055cd2ab9d5573caae7d5d02ba157449810f4e4d6573685f476966745f43617264004c011e581cb77934706fa311b6568d1070c2d23f092324b35ad623aa571a0e3726000159023c59023901000033323232323232322322232323225333009323232533300c3007300d3754002264646464a666026602c00426464a666024601a60266ea803854ccc048c034c04cdd5191980080080311299980b8008a60103d87a80001323253330163375e603660306ea800804c4cdd2a40006603400497ae0133004004001301b002301900115333012300c00113371e00402029405854ccc048cdc3800a4002266e3c0080405281bad3013002375c60220022c602800264a66601e601260206ea800452f5bded8c026eacc050c044dd500099191980080099198008009bab3016301730173017301700522533301500114bd6f7b630099191919299980b19b91488100002153330163371e9101000021003100513301a337606ea4008dd3000998030030019bab3017003375c602a0046032004602e00244a666028002298103d87a800013232323253330153372200e0042a66602a66e3c01c0084cdd2a4000660326e980052f5c02980103d87a80001330060060033756602c0066eb8c050008c060008c058004dd7180998081baa00337586024002601c6ea800858c040c044008c03c004c02cdd50008a4c26cac64a66601060060022a66601660146ea8010526161533300830020011533300b300a37540082930b0b18041baa003370e90011b8748000dd7000ab9a5573aaae7955cfaba05742ae8930010f4e4d6573685f476966745f43617264004c012bd8799fd8799f58203159a6f2ae24c5bfbed947fe0ecfe936f088c8d265484e6979cacb607d33c811ff05ff0001058284000040821a006acfc01ab2d05e00840100d87a80821a006acfc01ab2d05e00f5f6'
Expand All @@ -61,7 +57,6 @@ const foreignTx = Serialization.TxCBOR(
const createWalletAndApiWithStores = async (
unspendableUtxos: Cardano.Utxo[],
providers?: TestProviders,
getCollateralCallback?: (args: GetCollateralCallbackParams) => Promise<Cardano.Utxo[]>,
settle = true,
availableUtxos?: Cardano.Utxo[]
) => {
Expand All @@ -79,8 +74,7 @@ const createWalletAndApiWithStores = async (
const confirmationCallback = {
signData: createMockGenericCallback({ cancel$: NEVER }),
signTx: createMockGenericCallback({ cancel$: NEVER }),
submitTx: createMockGenericCallback(true),
...(!!getCollateralCallback && { getCollateral: getCollateralCallback })
submitTx: createMockGenericCallback(true)
};
wallet.governance.getPubDRepKey = jest.fn(wallet.governance.getPubDRepKey);

Expand Down Expand Up @@ -123,12 +117,7 @@ describe('cip30', () => {
})
);
// CREATE A WALLET
({ wallet, api, confirmationCallback } = await createWalletAndApiWithStores(
[mockUtxo[2]],
providers,
undefined,
false
));
({ wallet, api, confirmationCallback } = await createWalletAndApiWithStores([mockUtxo[2]], providers, false));
});

afterEach(() => {
Expand Down Expand Up @@ -302,26 +291,6 @@ describe('cip30', () => {
let wallet4: BaseWallet;
let api4: WithSenderContext<WalletApi>;

// Wallet 5
let wallet5: BaseWallet;
let api5: WithSenderContext<WalletApi>;

// Wallet 6
let wallet6: BaseWallet;
let api6: WithSenderContext<WalletApi>;

// Wallet 7
let wallet7: BaseWallet;
let api7: WithSenderContext<WalletApi>;

// Wallet 8
let wallet8: BaseWallet;
let api8: WithSenderContext<WalletApi>;

// Wallet 9
let wallet9: BaseWallet;
let api9: WithSenderContext<WalletApi>;

beforeAll(async () => {
// CREATE A WALLET WITH LOW COINS UTxOs
({ wallet: wallet2, api: api2 } = await createWalletAndApiWithStores(utxosWithLowCoins));
Expand All @@ -331,51 +300,6 @@ describe('cip30', () => {

// CREATE A WALLET WITH UTxOS WITH ASSETS
({ wallet: wallet4, api: api4 } = await createWalletAndApiWithStores([mockUtxo[1], mockUtxo[2]]));

// CREATE WALLET WITH CALLBACK FOR GET COLLATERAL (UNSPENDABLES DOES NOT FULFILL AMOUNT, AVAILABLE UTxOs WITH MIXED ASSETS)
({ wallet: wallet5, api: api5 } = await createWalletAndApiWithStores(
utxosWithLowCoins,
providers,
mockCollateralCallback,
true,
utxosWithLowCoinsAndMixedAssets
));

// CREATE WALLET WITH CALLBACK FOR GET COLLATERAL (NO UNSPENDABLES, AVAILABLE UTxOs WITH MIXED ASSETS)
({ wallet: wallet6, api: api6 } = await createWalletAndApiWithStores(
[],
providers,
mockCollateralCallback,
true,
utxosWithLowCoinsAndMixedAssets
));

// WALLET WITH CALLBACK FOR GET COLLATERAL (UNSPENDABLES DOES NOT FULFILL AMOUNT, NO AVAILABLE UTxOS)
({ wallet: wallet7, api: api7 } = await createWalletAndApiWithStores(
utxosWithLowCoins,
providers,
mockCollateralCallback,
true,
[]
));

// WALLET WITH CALLBACK FOR GET COLLATERAL (BRAND NEW WALLET, NO UTXOS)
({ wallet: wallet8, api: api8 } = await createWalletAndApiWithStores(
[],
providers,
mockCollateralCallback,
true,
[]
));

// WALLET WITH CALLBACK FOR GET COLLATERAL (ONLY IMPURE UTXOs)
({ wallet: wallet9, api: api9 } = await createWalletAndApiWithStores(
[],
providers,
mockCollateralCallback,
true,
impureUtxos
));
});

afterAll(() => {
Expand All @@ -384,81 +308,11 @@ describe('cip30', () => {
wallet4.shutdown();
});

beforeEach(() => {
mockCollateralCallback.mockClear();
});

test('can handle serialization errors', async () => {
// YYYY is invalid hex that will throw at serialization
await expect(api.getCollateral(context, { amount: 'YYYY' })).rejects.toThrowError(ApiError);
});

it('executes collateral callback if provided and unspendable UTxOs do not meet amount required', async () => {
const collateral = await api5.getCollateral(context);
expect(mockCollateralCallback).toHaveBeenCalledWith({
data: {
amount: 5_000_000n,
utxos: sortedUtxosWithLowCoins
},
sender: {
url: 'https://lace.io'
},
type: 'get_collateral'
});

expect(collateral).toEqual([Serialization.TransactionUnspentOutput.fromCore(mockUtxo[3]).toCbor()]);
wallet5.shutdown();
});

it('executes collateral callback if provided and no unspendable UTxOs are available', async () => {
const collateral = await api6.getCollateral(context);
expect(mockCollateralCallback).toHaveBeenCalledWith({
data: {
amount: 5_000_000n,
utxos: sortedUtxosWithLowCoins
},
sender: {
url: 'https://lace.io'
},
type: 'get_collateral'
});

expect(collateral).toEqual([Serialization.TransactionUnspentOutput.fromCore(mockUtxo[3]).toCbor()]);
wallet6.shutdown();
});

it('does not execute collateral callback if provided with no available UTxOs', async () => {
await expect(api7.getCollateral(context)).rejects.toThrow(ApiError);
expect(mockCollateralCallback).not.toHaveBeenCalled();
wallet7.shutdown();
});

it('does not execute collateral callback and returns null if brand new wallet (no UTXOS)', async () => {
await expect(api8.getCollateral(context)).resolves.toBeNull();
expect(mockCollateralCallback).not.toHaveBeenCalled();
wallet8.shutdown();
});

it('does executes collateral callback with empty array if wallet has only impure UTXOS', async () => {
await expect(api9.getCollateral(context)).resolves.not.toBeNull();
expect(mockCollateralCallback).toHaveBeenCalledWith({
data: {
amount: 5_000_000n,
utxos: []
},
sender: {
url: 'https://lace.io'
},
type: 'get_collateral'
});
wallet9.shutdown();
});

it('does not execute collateral callback if not provided', async () => {
await expect(api2.getCollateral(context)).rejects.toThrow(ApiError);
expect(mockCollateralCallback).not.toHaveBeenCalled();
});

test('accepts amount as tagged integer', async () => {
await expect(api.getCollateral(context, { amount: 'c2434c4b40' })).resolves.not.toThrow();
});
Expand Down
Loading