Skip to content

Commit

Permalink
Merge pull request #918 from input-output-hk/feat/LW-7403-distributio…
Browse files Browse the repository at this point in the history
…n-portfolio-locally-persisted-v2

feat!: delegation distribution portfolio is now persisted on chain an…
  • Loading branch information
rhyslbw committed Sep 20, 2023
2 parents 6593cfa + 7573938 commit dff6ca9
Show file tree
Hide file tree
Showing 14 changed files with 672 additions and 62 deletions.
30 changes: 30 additions & 0 deletions packages/core/src/Cardano/types/DelegationsAndRewards.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Lovelace } from './Value';
import { Metadatum } from './AuxiliaryData';
import { PoolId, PoolIdHex, StakePool } from './StakePool';
import { RewardAccount } from '../Address';
import { metadatumToJson } from '../../util/metadatum';

export interface DelegationsAndRewards {
delegate?: PoolId;
Expand Down Expand Up @@ -44,3 +46,31 @@ export interface Cip17DelegationPortfolio {
description?: string;
author?: string;
}

// On chain portfolio metadata
export const DelegationMetadataLabel = 6862n; // 0x1ace
export type DelegationPortfolioMetadata = Exclude<Cip17DelegationPortfolio, 'pools'> & {
pools: Pick<Cip17Pool, 'id' | 'weight'>[];
};

export const portfolioMetadataFromCip17 = (cip17: Cip17DelegationPortfolio): DelegationPortfolioMetadata => {
const portfolio = { ...cip17 };

portfolio.pools = cip17.pools.map((pool) => ({
id: pool.id,
weight: pool.weight
}));

return portfolio as DelegationPortfolioMetadata;
};

export const cip17FromMetadatum = (portfolio: Metadatum): Cip17DelegationPortfolio => {
const cip17 = metadatumToJson(portfolio);

for (const pool of cip17.pools) {
// Metadatum serializes/deserializes numbers as bigints
pool.weight = Number(pool.weight);
}

return cip17 as Cip17DelegationPortfolio;
};
98 changes: 98 additions & 0 deletions packages/core/test/Cardano/types/DelegationAndRewards.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as Cardano from '../../../src/Cardano';

describe('portfolioMetadataFromCip17', () => {
const poolIds: Cardano.PoolId[] = [
Cardano.PoolId('pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh'),
Cardano.PoolId('pool1t9xlrjyk76c96jltaspgwcnulq6pdkmhnge8xgza8ku7qvpsy9r'),
Cardano.PoolId('pool1la4ghj4w4f8p4yk4qmx0qvqmzv6592ee9rs0vgla5w6lc2nc8w5')
];

const portfolio = {
// eslint-disable-next-line sonarjs/no-duplicate-string
name: 'Tests Portfolio',
pools: [
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[0])),
name: 'A',
ticker: 'At',
weight: 1
},
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[1])),
name: 'B',
ticker: 'Bt',
weight: 1
},
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[2])),
name: 'C',
ticker: 'Ct',
weight: 1
}
]
};

it('can get portfolio metadata from CIP-17', () => {
const portfolioMetadata = Cardano.portfolioMetadataFromCip17(portfolio);

expect(portfolioMetadata).toEqual({
name: 'Tests Portfolio',
pools: [
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[0])),
weight: 1
},
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[1])),
weight: 1
},
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[2])),
weight: 1
}
]
});
});

it('can get portfolio metadata from CIP-17', () => {
const metadatum: Cardano.Metadatum = new Map<Cardano.Metadatum, Cardano.Metadatum>([
['name', 'Tests Portfolio'],
[
'pools',
[
new Map<Cardano.Metadatum, Cardano.Metadatum>([
['id', Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[0]))],
['weight', 1n]
]),
new Map<Cardano.Metadatum, Cardano.Metadatum>([
['id', Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[1]))],
['weight', 1n]
]),
new Map<Cardano.Metadatum, Cardano.Metadatum>([
['id', Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[2]))],
['weight', 1n]
])
]
]
]);

const cip17 = Cardano.cip17FromMetadatum(metadatum);
expect(cip17).toEqual({
name: 'Tests Portfolio',
pools: [
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[0])),
weight: 1
},
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[1])),
weight: 1
},
{
id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolIds[2])),
weight: 1
}
]
});
});
});
1 change: 1 addition & 0 deletions packages/e2e/test/long-running/delegation-rewards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const submitDelegationTx = async (wallet: PersonalWallet, pools: Cardano.PoolId[
const { tx: signedTx } = await wallet
.createTxBuilder()
.delegatePortfolio({
name: 'Test Portfolio',
pools: pools.map((poolId) => ({ id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolId)), weight: 1 }))
})
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,15 @@ const delegateToMultiplePools = async (
weights = Array.from({ length: POOLS_COUNT }).map(() => 1)
) => {
const poolIds = await getPoolIds(wallet);
const portfolio: Pick<Cardano.Cip17DelegationPortfolio, 'pools'> = {
const portfolio: Cardano.Cip17DelegationPortfolio = {
name: 'Test Portfolio',
pools: poolIds.map(({ hexId: id }, idx) => ({ id, weight: weights[idx] }))
};
logger.debug('Delegating portfolio', portfolio);

const { tx } = await wallet.createTxBuilder().delegatePortfolio(portfolio).build().sign();
await submitAndConfirm(wallet, tx);
return poolIds;
return { poolIds, portfolio };
};

const delegateAllToSinglePool = async (wallet: PersonalWallet): Promise<void> => {
Expand Down Expand Up @@ -138,10 +139,12 @@ describe('PersonalWallet/delegationDistribution', () => {

// No stake distribution initially
const delegationDistribution = await firstValueFrom(wallet.delegation.distribution$);
const delegationPortfolio = await firstValueFrom(wallet.delegation.portfolio$);
logger.info('Empty delegation distribution initially');
expect(delegationDistribution).toEqual(new Map());
expect(delegationPortfolio).toEqual(null);

const poolIds = await delegateToMultiplePools(wallet);
const { poolIds, portfolio } = await delegateToMultiplePools(wallet);
const walletAddresses = await firstValueFromTimed(wallet.addresses$);
const rewardAccounts = await firstValueFrom(wallet.delegation.rewardAccounts$);

Expand Down Expand Up @@ -173,11 +176,13 @@ describe('PersonalWallet/delegationDistribution', () => {
stake: perAddrBalance[index]
}));
const actualDelegationDistribution = await firstValueFrom(wallet.delegation.distribution$);
const actualPortfolio = await firstValueFrom(wallet.delegation.portfolio$);

logger.info('Funds were distributed evenly across the addresses.');
logger.info(distributionMessage, actualDelegationDistribution);

expect([...actualDelegationDistribution.values()]).toEqual(expectedDelegationDistribution);
expect(portfolio).toEqual(actualPortfolio);

// Delegate so that last address has all funds
await delegateToMultiplePools(
Expand Down Expand Up @@ -229,6 +234,7 @@ describe('PersonalWallet/delegationDistribution', () => {
)
)
);

expect(simplifiedDelegationDistribution).toEqual([
{
id: poolIds[0].id,
Expand All @@ -237,5 +243,10 @@ describe('PersonalWallet/delegationDistribution', () => {
rewardAccounts: rewardAccounts.map(({ address }) => address)
}
]);

// The simplified portfolio transaction doesn't send a portfolio but rather simply delegates all funds to one pool manually.
// This emulates the case where a user syncs its seeds phrases into another wallet and delegates to a single pool (without attaching metadata),
// this action undoes the portfolio, we should get null here.
expect(await firstValueFrom(wallet.delegation.portfolio$)).toBe(null);
});
});
21 changes: 19 additions & 2 deletions packages/tx-construction/src/tx-builder/TxBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Crypto from '@cardano-sdk/crypto';
import { Cardano, HandleProvider, HandleResolution } from '@cardano-sdk/core';
import { Cardano, HandleProvider, HandleResolution, metadatum } from '@cardano-sdk/core';
import { GreedyInputSelector, SelectionSkeleton } from '@cardano-sdk/input-selection';
import { GroupedAddress, SignTransactionOptions, TransactionSigner, util } from '@cardano-sdk/key-management';
import {
Expand Down Expand Up @@ -144,14 +144,31 @@ export class GenericTxBuilder implements TxBuilder {
});
}

delegatePortfolio(portfolio: Pick<Cardano.Cip17DelegationPortfolio, 'pools'> | null): TxBuilder {
delegatePortfolio(portfolio: Cardano.Cip17DelegationPortfolio | null): TxBuilder {
if (portfolio?.pools.length === 0) {
throw new Error('Portfolio should define at least one delegation pool.');
}
this.#requestedPortfolio = (portfolio?.pools ?? []).map((pool) => ({
...pool,
id: Cardano.PoolId.fromKeyHash(pool.id as unknown as Crypto.Ed25519KeyHashHex)
}));

if (portfolio) {
if (this.partialAuxiliaryData?.blob) {
this.partialAuxiliaryData.blob.set(
Cardano.DelegationMetadataLabel,
metadatum.jsonToMetadatum(Cardano.portfolioMetadataFromCip17(portfolio))
);
} else {
this.partialAuxiliaryData = {
...this.partialAuxiliaryData,
blob: new Map([
[Cardano.DelegationMetadataLabel, metadatum.jsonToMetadatum(Cardano.portfolioMetadataFromCip17(portfolio))]
])
};
}
}

return this;
}

Expand Down
Loading

0 comments on commit dff6ca9

Please sign in to comment.