Skip to content

Commit

Permalink
feat(sdk-coin-ada): support vote delegation
Browse files Browse the repository at this point in the history
  • Loading branch information
ryandengBG committed Dec 12, 2024
1 parent 7e4f921 commit cb2c809
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 58 deletions.
1 change: 1 addition & 0 deletions modules/sdk-coin-ada/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export { StakingClaimRewardsBuilder } from './stakingClaimRewardsBuilder';
export { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
export { StakingWithdrawBuilder } from './stakingWithdrawBuilder';
export { StakingPledgeBuilder } from './stakingPledgeBuilder';
export { VoteDelegationBuilder } from './voteDelegationBuilder';
export { Utils };
7 changes: 6 additions & 1 deletion modules/sdk-coin-ada/src/lib/stakingActivateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction';
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
import adaUtils from './utils';

export class StakingActivateBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand All @@ -25,7 +26,7 @@ export class StakingActivateBuilder extends TransactionBuilder {
* @param stakingPublicKey The user's public stake key
* @param poolHash Pool ID Hash of the pool we are going to delegate to
*/
stakingCredential(stakingPublicKey: string, poolHash: string): this {
stakingCredential(stakingPublicKey: string, poolHash: string, dRepId: string): this {
const stakeCredential = CardanoWasm.Credential.from_keyhash(
CardanoWasm.PublicKey.from_bytes(Buffer.from(stakingPublicKey, 'hex')).hash()
);
Expand All @@ -40,6 +41,10 @@ export class StakingActivateBuilder extends TransactionBuilder {
)
);
this._certs.push(stakeDelegationCert);
const voteDelegationCert = CardanoWasm.Certificate.new_vote_delegation(
CardanoWasm.VoteDelegation.new(stakeCredential, adaUtils.getDRepFromDRepId(dRepId))
);
this._certs.push(voteDelegationCert);
return this;
}

Expand Down
17 changes: 16 additions & 1 deletion modules/sdk-coin-ada/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
import { KeyPair } from './keyPair';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import adaUtils from './utils';

export interface TransactionInput {
transaction_id: string;
Expand All @@ -31,17 +32,19 @@ export interface Witness {
publicKey: string;
signature: string;
}
enum CertType {
export enum CertType {
StakeKeyRegistration,
StakeKeyDelegation,
StakeKeyDeregistration,
StakePoolRegistration,
VoteDelegation,
}

export interface Cert {
type: CertType;
stakeCredentialHash?: string;
poolKeyHash?: string;
dRepId?: string;
}

export interface Withdrawal {
Expand Down Expand Up @@ -183,6 +186,14 @@ export class Transaction extends BaseTransaction {
poolKeyHash: Buffer.from(stakePoolRegistration.pool_params().operator().to_bytes()).toString('hex'),
});
}
if (cert.as_vote_delegation() !== undefined) {
const voteDelegation = cert.as_vote_delegation() as CardanoWasm.VoteDelegation;
result.certs.push({
type: CertType.VoteDelegation,
stakeCredentialHash: Buffer.from(voteDelegation.stake_credential().to_bytes()).toString('hex'),
dRepId: adaUtils.getDRepIdFromDRep(voteDelegation.drep()),
});
}
}
}

Expand Down Expand Up @@ -279,6 +290,8 @@ export class Transaction extends BaseTransaction {
this._type = TransactionType.StakingActivate;
} else if (certs.some((c) => c.as_stake_deregistration() !== undefined)) {
this._type = TransactionType.StakingDeactivate;
} else if (certs.some((c) => c.as_vote_delegation() !== undefined)) {
this._type = TransactionType.VoteDelegation;
}
}
if (this._transaction.body().withdrawals()) {
Expand Down Expand Up @@ -394,6 +407,8 @@ export class Transaction extends BaseTransaction {
? 'StakingDeactivate'
: this._type === TransactionType.StakingPledge
? 'StakingPledge'
: this._type === TransactionType.VoteDelegation
? 'VoteDelegation'
: 'undefined';
return {
displayOrder,
Expand Down
4 changes: 2 additions & 2 deletions modules/sdk-coin-ada/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {

const adjustment = BigNum.from_str('2000000');
let change = utxoBalance.checked_sub(this._fee).checked_sub(totalAmountToSend);
if (this._type === TransactionType.StakingActivate) {
if (this._type === TransactionType.StakingActivate || this._type === TransactionType.VoteDelegation) {
change = change.checked_sub(adjustment);
} else if (this._type === TransactionType.StakingDeactivate) {
change = change.checked_add(adjustment);
Expand Down Expand Up @@ -326,7 +326,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {

const adjustment = BigNum.from_str('2000000');
let change = utxoBalance.checked_sub(this._fee).checked_sub(totalAmountToSend);
if (this._type === TransactionType.StakingActivate) {
if (this._type === TransactionType.StakingActivate || this._type === TransactionType.VoteDelegation) {
change = change.checked_sub(adjustment);
} else if (this._type === TransactionType.StakingDeactivate) {
change = change.checked_add(adjustment);
Expand Down
15 changes: 11 additions & 4 deletions modules/sdk-coin-ada/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
import { StakingWithdrawBuilder } from './stakingWithdrawBuilder';
import { StakingPledgeBuilder } from './stakingPledgeBuilder';
import { StakingClaimRewardsBuilder } from './stakingClaimRewardsBuilder';
import { VoteDelegationBuilder } from './voteDelegationBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -38,6 +39,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.getWalletInitializationBuilder(tx);
case TransactionType.StakingPledge:
return this.getStakingPledgeBuilder(tx);
case TransactionType.VoteDelegation:
return this.getVoteDelegationBuilder(tx);
default:
throw new InvalidTransactionError('unsupported transaction');
}
Expand All @@ -60,19 +63,23 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return TransactionBuilderFactory.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig));
}

getStakingClaimRewardsBuilder(tx?: Transaction) {
getVoteDelegationBuilder(tx?: Transaction): VoteDelegationBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new VoteDelegationBuilder(this._coinConfig));
}

getStakingClaimRewardsBuilder(tx?: Transaction): StakingClaimRewardsBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new StakingClaimRewardsBuilder(this._coinConfig));
}

getStakingDeactivateBuilder(tx?: Transaction) {
getStakingDeactivateBuilder(tx?: Transaction): StakingDeactivateBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig));
}

getStakingWithdrawBuilder(tx?: Transaction) {
getStakingWithdrawBuilder(tx?: Transaction): StakingWithdrawBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new StakingWithdrawBuilder(this._coinConfig));
}

getStakingPledgeBuilder(tx?: Transaction) {
getStakingPledgeBuilder(tx?: Transaction): StakingPledgeBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new StakingPledgeBuilder(this._coinConfig));
}

Expand Down
66 changes: 66 additions & 0 deletions modules/sdk-coin-ada/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import {
Credential,
RewardAddress,
Transaction as CardanoTransaction,
DRep,
Ed25519KeyHash,
ScriptHash,
DRepKind,
} from '@emurgo/cardano-serialization-lib-nodejs';
import { KeyPair } from './keyPair';
import { bech32 } from 'bech32';

export const MIN_ADA_FOR_ONE_ASSET = '1500000';
export const VOTE_ALWAYS_ABSTAIN = 'always-abstain';
export const VOTE_ALWAYS_NO_CONFIDENCE = 'always-no-confidence';
export const DEFAULT_VOTE_OPTIONS = [VOTE_ALWAYS_ABSTAIN, VOTE_ALWAYS_NO_CONFIDENCE];

export class Utils implements BaseUtils {
createBaseAddressWithStakeAndPaymentKey(
Expand Down Expand Up @@ -75,6 +82,65 @@ export class Utils implements BaseUtils {
return rewardAddress.to_address().to_bech32();
}

isValidDRepId(dRepId: string): boolean {
try {
this.getDRepFromDRepId(dRepId);
return true;
} catch (err) {
return false;
}
}

getDRepFromDRepId(dRepId: string): DRep {
switch (dRepId) {
case 'always-abstain':
return DRep.new_always_abstain();
case 'always-no-confidence':
return DRep.new_always_no_confidence();
default:
try {
// for parsing CIP-105 standard DRep ID
return DRep.from_bech32(dRepId);
} catch (err) {
// for parsing CIP-129 standard DRep ID
// https://cips.cardano.org/cip/CIP-0129
const decodedBech32 = bech32.decode(dRepId);
const decodedBytes = Buffer.from(bech32.fromWords(decodedBech32.words));
const header = decodedBytes[0];
const keyBytes = decodedBytes.subarray(1);

const keyType = (header & 0xf0) >> 4;
const credentialType = header & 0x0f;

if (keyType !== 0x02) {
throw new Error('Invalid key type for DRep');
}

switch (credentialType) {
case 0x02:
const ed25519KeyHash = Ed25519KeyHash.from_bytes(keyBytes);
return DRep.new_key_hash(ed25519KeyHash);
case 0x03:
const scriptHash = ScriptHash.from_bytes(keyBytes);
return DRep.new_script_hash(scriptHash);
default:
throw new Error('Invalid credential type for DRep');
}
}
}
}

getDRepIdFromDRep(dRep: DRep): string {
switch (dRep.kind()) {
case DRepKind.AlwaysAbstain:
return VOTE_ALWAYS_ABSTAIN;
case DRepKind.AlwaysNoConfidence:
return VOTE_ALWAYS_NO_CONFIDENCE;
default:
return dRep.to_bech32();
}
}

/** @inheritdoc */
// this will validate both stake and payment addresses
isValidAddress(address: string): boolean {
Expand Down
56 changes: 56 additions & 0 deletions modules/sdk-coin-ada/src/lib/voteDelegationBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { BaseKey, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction';
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
import adaUtils from './utils';

export class VoteDelegationBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._type = TransactionType.VoteDelegation;
}

protected get transactionType(): TransactionType {
return TransactionType.VoteDelegation;
}

/** @inheritdoc */
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
}

/**
* Creates the proper certificates needed to delegate a user's vote to a given DRep
*
* @param stakingPublicKey The user's public stake key
* @param dRepId The DRep ID of the DRep we will delegate vote to
*/
addVoteDelegationCertificate(stakingPublicKey: string, dRepId: string): this {
const stakeCredential = CardanoWasm.Credential.from_keyhash(
CardanoWasm.PublicKey.from_bytes(Buffer.from(stakingPublicKey, 'hex')).hash()
);
const voteDelegationCert = CardanoWasm.Certificate.new_vote_delegation(
CardanoWasm.VoteDelegation.new(stakeCredential, adaUtils.getDRepFromDRepId(dRepId))
);
this._certs.push(voteDelegationCert);
return this;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
const tx = await super.buildImplementation();
tx.setTransactionType(TransactionType.VoteDelegation);
return tx;
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
return super.fromImplementation(rawTransaction);
}

/** @inheritdoc */
protected signImplementation(key: BaseKey): Transaction {
return super.signImplementation(key);
}
}
11 changes: 8 additions & 3 deletions modules/sdk-coin-ada/test/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,16 @@ export const rawTx = {
address: 'addr_test1vr8rakm66rcfv4fcxqykg5lf0yv7lsyk9mvapx369jpvtcgfcuk7f',
value: '248329150',
},
unsignedVoteDelegationTx:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001bf227021a00029259031a2faf0800048183098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8200581c8b75035882d4165bea8000c4d3f2c123ae33c1d92a751a78135a2402a0f5f6',
unsignedVoteDelegationTxBody:
'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001bf227021a00029259031a2faf0800048183098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8200581c8b75035882d4165bea8000c4d3f2c123ae33c1d92a751a78135a2402',
unsignedVoteDelegationTxHash: 'c33c503919f9c5aa5a4ca40c0c6e4bb0099a7452a093d9ee01aff93958415e93',
unsignedStakingActiveTx:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001beca7021a000297d9031a2faf0800048282008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3a0f5f6',
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001be677021a00029e09031a2faf0800048382008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b383098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8102a0f5f6',
unsignedStakingActiveTxBody:
'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001beca7021a000297d9031a2faf0800048282008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3',
unsignedStakingActiveTxHash: '4e2777152ebc92c73daebc0075cee8a52ca9fb8c00511ac2c035b58c52f27125',
'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001be677021a00029e09031a2faf0800048382008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b383098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8102',
unsignedStakingActiveTxHash: 'd8ecaa7bfee2e28691673be378ea6583bc8efd5a6e29ca9cfb278279a06dd216',
unsignedStakingDeactiveTx:
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a005900a7021a00028cd9031a2faf0800048182018200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdba0f5f6',
unsignedStakingDeactiveTxBody:
Expand Down
Loading

0 comments on commit cb2c809

Please sign in to comment.