Skip to content

Commit 143461f

Browse files
fix(wallet): dynamicChangeResolver gives preference to lower derivation indices
1 parent 7c2d694 commit 143461f

File tree

3 files changed

+135
-4
lines changed

3 files changed

+135
-4
lines changed

packages/wallet/src/services/ChangeAddress/DynamicChangeAddressResolver.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,33 @@ export const delegationMatchesPortfolio = (
284284
/** Gets the current delegation portfolio. */
285285
export type GetDelegationPortfolio = () => Promise<Cardano.Cip17DelegationPortfolio | null>;
286286

287+
/**
288+
* Sorts an array of addresses by their primary index and, if available, by the
289+
* index of their stakeKeyDerivationPath.
290+
*
291+
* @param addresses - The array of addresses to sort.
292+
* @returns A new sorted array of addresses.
293+
*/
294+
const sortAddresses = (addresses: GroupedAddress[]): GroupedAddress[] =>
295+
[...addresses].sort((a, b) => {
296+
if (a.index !== b.index) {
297+
return a.index - b.index;
298+
}
299+
300+
if (a.stakeKeyDerivationPath && b.stakeKeyDerivationPath) {
301+
return a.stakeKeyDerivationPath.index - b.stakeKeyDerivationPath.index;
302+
}
303+
304+
if (a.stakeKeyDerivationPath && !b.stakeKeyDerivationPath) {
305+
return -1;
306+
}
307+
if (!a.stakeKeyDerivationPath && b.stakeKeyDerivationPath) {
308+
return 1;
309+
}
310+
311+
return 0;
312+
});
313+
287314
/** Resolves the address to be used for change. */
288315
export class DynamicChangeAddressResolver implements ChangeAddressResolver {
289316
readonly #getDelegationPortfolio: GetDelegationPortfolio;
@@ -315,7 +342,8 @@ export class DynamicChangeAddressResolver implements ChangeAddressResolver {
315342
async resolve(selection: Selection): Promise<Array<Cardano.TxOut>> {
316343
const delegationDistribution = [...(await firstValueFrom(this.#delegationDistribution)).values()];
317344
let portfolio = await this.#getDelegationPortfolio();
318-
const addresses = await firstValueFrom(this.#addresses$);
345+
const addresses = sortAddresses(await firstValueFrom(this.#addresses$));
346+
319347
let updatedChange = [...selection.change];
320348

321349
if (addresses.length === 0) throw new InvalidStateError('The wallet has no known addresses.');

packages/wallet/test/services/ChangeAddress/DynamicChangeAddressResolver.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
rewardAccount_0,
2626
rewardAccount_1,
2727
rewardAccount_2,
28-
rewardAccount_3
28+
rewardAccount_3,
29+
unorderedKnownAddresses$
2930
} from './testData';
3031
import { logger } from '@cardano-sdk/util-dev';
3132

@@ -154,7 +155,7 @@ describe('DynamicChangeAddressResolver', () => {
154155

155156
it('adds all change outputs at payment_stake address 0 if the wallet is currently not delegating to any pool', async () => {
156157
const changeAddressResolver = new DynamicChangeAddressResolver(
157-
knownAddresses$,
158+
unorderedKnownAddresses$,
158159
createMockDelegateTracker(new Map<Cardano.PoolId, DelegatedStake>([])).distribution$,
159160
getNullDelegationPortfolio,
160161
logger
@@ -191,7 +192,7 @@ describe('DynamicChangeAddressResolver', () => {
191192

192193
it('distributes change equally between the currently delegated addresses if no portfolio is given, ', async () => {
193194
const changeAddressResolver = new DynamicChangeAddressResolver(
194-
knownAddresses$,
195+
unorderedKnownAddresses$,
195196
createMockDelegateTracker(
196197
new Map<Cardano.PoolId, DelegatedStake>([
197198
[

packages/wallet/test/services/ChangeAddress/testData.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,108 @@ export const knownAddresses$ = new BehaviorSubject<GroupedAddress[]>([
212212
}
213213
]);
214214

215+
export const unorderedKnownAddresses$ = new BehaviorSubject<GroupedAddress[]>([
216+
{
217+
accountIndex: 0,
218+
address: address_5_0,
219+
index: 5,
220+
networkId: Cardano.NetworkId.Testnet,
221+
rewardAccount: rewardAccount_0,
222+
stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake },
223+
type: AddressType.External
224+
},
225+
{
226+
accountIndex: 0,
227+
address: address_1_0,
228+
index: 1,
229+
networkId: Cardano.NetworkId.Testnet,
230+
rewardAccount: rewardAccount_0,
231+
stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake },
232+
type: AddressType.External
233+
},
234+
{
235+
accountIndex: 0,
236+
address: address_0_3,
237+
index: 0,
238+
networkId: Cardano.NetworkId.Testnet,
239+
rewardAccount: rewardAccount_3,
240+
stakeKeyDerivationPath: { index: 3, role: KeyRole.Stake },
241+
type: AddressType.External
242+
},
243+
{
244+
accountIndex: 0,
245+
address: address_0_1,
246+
index: 0,
247+
networkId: Cardano.NetworkId.Testnet,
248+
rewardAccount: rewardAccount_1,
249+
stakeKeyDerivationPath: { index: 1, role: KeyRole.Stake },
250+
type: AddressType.External
251+
},
252+
{
253+
accountIndex: 0,
254+
address: address_0_2,
255+
index: 0,
256+
networkId: Cardano.NetworkId.Testnet,
257+
rewardAccount: rewardAccount_2,
258+
stakeKeyDerivationPath: { index: 2, role: KeyRole.Stake },
259+
type: AddressType.External
260+
},
261+
{
262+
accountIndex: 0,
263+
address: address_2_0,
264+
index: 2,
265+
networkId: Cardano.NetworkId.Testnet,
266+
rewardAccount: rewardAccount_0,
267+
stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake },
268+
type: AddressType.External
269+
},
270+
{
271+
accountIndex: 0,
272+
address: address_0_4,
273+
index: 0,
274+
networkId: Cardano.NetworkId.Testnet,
275+
rewardAccount: rewardAccount_4,
276+
stakeKeyDerivationPath: { index: 4, role: KeyRole.Stake },
277+
type: AddressType.External
278+
},
279+
{
280+
accountIndex: 0,
281+
address: address_3_0,
282+
index: 3,
283+
networkId: Cardano.NetworkId.Testnet,
284+
rewardAccount: rewardAccount_0,
285+
stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake },
286+
type: AddressType.External
287+
},
288+
{
289+
accountIndex: 0,
290+
address: address_0_5,
291+
index: 0,
292+
networkId: Cardano.NetworkId.Testnet,
293+
rewardAccount: rewardAccount_5,
294+
stakeKeyDerivationPath: { index: 5, role: KeyRole.Stake },
295+
type: AddressType.External
296+
},
297+
{
298+
accountIndex: 0,
299+
address: address_0_0,
300+
index: 0,
301+
networkId: Cardano.NetworkId.Testnet,
302+
rewardAccount: rewardAccount_0,
303+
stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake },
304+
type: AddressType.External
305+
},
306+
{
307+
accountIndex: 0,
308+
address: address_4_0,
309+
index: 4,
310+
networkId: Cardano.NetworkId.Testnet,
311+
rewardAccount: rewardAccount_0,
312+
stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake },
313+
type: AddressType.External
314+
}
315+
]);
316+
215317
export const emptyKnownAddresses$ = new BehaviorSubject<GroupedAddress[]>([]);
216318

217319
export const createMockDelegateTracker = (delegatedStake: Map<Cardano.PoolId, DelegatedStake>) => ({

0 commit comments

Comments
 (0)