Skip to content

Commit

Permalink
feat(tx-builder): delegatePortfolio creates certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
mirceahasegan committed Jun 5, 2023
1 parent 7488d14 commit fc88350
Show file tree
Hide file tree
Showing 6 changed files with 490 additions and 4 deletions.
15 changes: 14 additions & 1 deletion packages/core/src/Cardano/types/DelegationsAndRewards.ts
@@ -1,5 +1,5 @@
import { Lovelace } from './Value';
import { PoolId, StakePool } from './StakePool';
import { PoolId, PoolIdHex, StakePool } from './StakePool';
import { RewardAccount } from '../Address';

export interface DelegationsAndRewards {
Expand Down Expand Up @@ -31,3 +31,16 @@ export interface RewardAccountInfo {
rewardBalance: Lovelace;
// Maybe add rewardsHistory for each reward account too
}

export interface Cip17Pool {
id: PoolIdHex;
weight: number;
name?: string;
ticker?: string;
}
export interface Cip17DelegationPortfolio {
name: string;
pools: Cip17Pool[];
description?: string;
author?: string;
}
97 changes: 97 additions & 0 deletions packages/tx-construction/src/tx-builder/TxBuilder.ts
Expand Up @@ -15,6 +15,7 @@ import {
UnsignedTx
} from './types';
import { OutputBuilderValidator, TxOutputBuilder } from './OutputBuilder';
import { RewardAccountWithPoolId } from '../types';
import { SelectionSkeleton } from '@cardano-sdk/input-selection';
import { SignTransactionOptions, TransactionSigner } from '@cardano-sdk/key-management';
import { contextLogger, deepEquals } from '@cardano-sdk/util';
Expand Down Expand Up @@ -52,6 +53,8 @@ interface LazySignerProps {
signer: Signer;
}

type TxBuilderStakePool = Omit<Cardano.Cip17Pool, 'id'> & { id: Cardano.PoolId };

class LazyTxSigner implements UnsignedTx {
#built?: BuiltTx;
#signer: Signer;
Expand Down Expand Up @@ -89,6 +92,7 @@ export class GenericTxBuilder implements TxBuilder {
#dependencies: TxBuilderDependencies;
#outputValidator: OutputBuilderValidator;
#delegateConfig: DelegateConfig;
#requestedPortfolio: TxBuilderStakePool[];
#logger: Logger;
#handleProvider?: HandleProvider;
#handles: HandleResolution[];
Expand Down Expand Up @@ -147,6 +151,14 @@ export class GenericTxBuilder implements TxBuilder {
return this;
}

delegatePortfolio(portfolio: Cardano.Cip17DelegationPortfolio): TxBuilder {
this.#requestedPortfolio = portfolio.pools.map((pool) => ({
...pool,
id: Cardano.PoolId.fromKeyHash(pool.id as unknown as Crypto.Ed25519KeyHashHex)
}));
return this;
}

metadata(metadata: Cardano.TxMetadata): TxBuilder {
this.partialAuxiliaryData = { ...this.partialAuxiliaryData, blob: new Map(metadata) };
return this;
Expand All @@ -169,6 +181,7 @@ export class GenericTxBuilder implements TxBuilder {
this.#logger.debug('Building');
try {
await this.#addDelegationCertificates();
await this.#delegatePortfolio();
await this.#validateOutputs();
// Take a snapshot of returned properties,
// so that they don't change while `initializeTx` is resolving
Expand Down Expand Up @@ -287,6 +300,90 @@ export class GenericTxBuilder implements TxBuilder {
}
}

async #delegatePortfolio(): Promise<void> {
const rewardAccounts = await this.#dependencies.txBuilderProviders.rewardAccounts();
// New poolIds will be allocated to un-delegated stake keys
const newPoolIds = this.#requestedPortfolio
.filter((cip17Pool) =>
rewardAccounts.every((rewardAccount) => rewardAccount.delegatee?.nextNextEpoch?.id !== cip17Pool.id)
)
.map(({ id }) => id);

// Reward accounts which don't have the stake key registered or that were delegated but should not be anymore
// Code below will pop items one by one (poolId)-(available stake key), so it will move the ones that are
// already delegated at the end, so that they are the first ones to be picked up in case of
// delegation switch to one of the poolIds from the portfolio
const availableRewardAccounts = rewardAccounts
.filter(
(rewardAccount) =>
rewardAccount.keyStatus === Cardano.StakeKeyStatus.Unregistered ||
!rewardAccount.delegatee?.nextNextEpoch ||
this.#requestedPortfolio.every(({ id }) => id !== rewardAccount.delegatee?.nextNextEpoch?.id)
)
.sort(GenericTxBuilder.#sortRewardAccountsDelegatedLast);

if (newPoolIds.length > availableRewardAccounts.length) {
throw new Error(
`Insufficient available reward accounts (${availableRewardAccounts.length}) for the requested number of delegations (${newPoolIds.length})`
);
}

const certificates: Cardano.Certificate[] = [];
while (newPoolIds.length > 0 && availableRewardAccounts.length > 0) {
const newPoolId = newPoolIds.pop()!;
const rewardAccount = availableRewardAccounts.pop()!;
const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount.address);
if (rewardAccount.keyStatus !== Cardano.StakeKeyStatus.Registered) {
certificates.push({
__typename: Cardano.CertificateType.StakeKeyRegistration,
stakeKeyHash
});
}
certificates.push(GenericTxBuilder.#createDelegationCert(newPoolId, stakeKeyHash));
}

// Unregister stake keys no longer needed
for (const rewardAccount of availableRewardAccounts) {
if (rewardAccount.keyStatus !== Cardano.StakeKeyStatus.Unregistered) {
certificates.push({
__typename: Cardano.CertificateType.StakeKeyDeregistration,
stakeKeyHash: Cardano.RewardAccount.toHash(rewardAccount.address)
});
}
}
this.partialTxBody = { ...this.partialTxBody, certificates };
}

static #sortRewardAccountsDelegatedLast(a: RewardAccountWithPoolId, b: RewardAccountWithPoolId): number {
if (a.keyStatus === b.keyStatus) {
if (a.keyStatus === Cardano.StakeKeyStatus.Unregistered) {
return 0; // both stake keys are unregistered. Order doesn't matter
}
// both stake keys are registered
if (a.delegatee?.nextNextEpoch) {
if (b.delegatee?.nextNextEpoch) {
// both stake keys are delegated to pools. Order doesn't matter
return 0;
}
// Only first one is delegated. It should be popped first in case of re-delegation
return 1;
}
if (b.delegatee?.nextNextEpoch) {
if (a.delegatee?.nextNextEpoch) {
// both stake keys are delegated to pools. Order doesn't matter
return 0;
}
// Only second one is delegated. It should be popped first in case of re-delegation
return -1;
}
}
if (a.keyStatus === Cardano.StakeKeyStatus.Registered) {
// Only first stake key is registered. It should be popped first for re-delegation
return 1;
}
return -1; // Only second stake key is registered. It should be popped first in case of re-delegation
}

static #createDelegationCert(
poolId: Cardano.PoolId,
stakeKeyHash: Crypto.Ed25519KeyHashHex
Expand Down
9 changes: 9 additions & 0 deletions packages/tx-construction/src/tx-builder/types.ts
Expand Up @@ -181,10 +181,19 @@ export interface TxBuilder {
* StakeKeyRegistration certificates are added in the transaction body.
* - Stake key deregister is done by not providing the `poolId` parameter: `delegate()`.
* - If wallet contains multiple reward accounts, it will create certificates for all of them.
* - It cannot be used in conjunction with {@link delegatePortfolio}
*
* @param poolId Pool Id to delegate to. If undefined, stake key deregistration will be done.
* @throws exception if used in conjunction with {@link delegatePortfolio}.
*/
delegate(poolId?: Cardano.PoolId): TxBuilder;
/**
* Configure the transaction to include all certificates needed to delegate to the pools from the portfolio.
* - It cannot be used in conjunction with {@link delegate}.
*
* @throws exception if used in conjunction with {@link delegate} call.
*/
delegatePortfolio(portfolio: Cardano.Cip17DelegationPortfolio): TxBuilder;
/** Sets TxMetadata in {@link auxiliaryData} */
metadata(metadata: Cardano.TxMetadata): TxBuilder;
/** Sets extra signers in {@link extraSigners} */
Expand Down
6 changes: 5 additions & 1 deletion packages/tx-construction/src/types.ts
Expand Up @@ -7,12 +7,16 @@ import { MinimumCoinQuantityPerOutput } from './output-validation';

export type InitializeTxResult = Cardano.TxBodyWithHash & { inputSelection: SelectionSkeleton };

export type RewardAccountWithPoolId = Omit<Cardano.RewardAccountInfo, 'delegatee'> & {
delegatee: { nextNextEpoch: { id: Cardano.PoolId } };
};

export interface TxBuilderProviders {
tip: () => Promise<Cardano.Tip>;
protocolParameters: () => Promise<Cardano.ProtocolParameters>;
changeAddress: () => Promise<Cardano.PaymentAddress>;
genesisParameters: () => Promise<Cardano.CompactGenesis>;
rewardAccounts: () => Promise<Omit<Cardano.RewardAccountInfo, 'delegatee'>[]>;
rewardAccounts: () => Promise<RewardAccountWithPoolId[]>;
utxoAvailable: () => Promise<Cardano.Utxo[]>;
}

Expand Down

0 comments on commit fc88350

Please sign in to comment.