Skip to content

Commit f166895

Browse files
authored
Merge pull request #5212 from BitGo/support-ada-vote-delegation
feat(ada): support vote delegation
2 parents d66fd82 + 6b9ddd8 commit f166895

File tree

11 files changed

+310
-17
lines changed

11 files changed

+310
-17
lines changed

modules/sdk-coin-ada/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export { StakingClaimRewardsBuilder } from './stakingClaimRewardsBuilder';
1010
export { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
1111
export { StakingWithdrawBuilder } from './stakingWithdrawBuilder';
1212
export { StakingPledgeBuilder } from './stakingPledgeBuilder';
13+
export { VoteDelegationBuilder } from './voteDelegationBuilder';
1314
export { Utils };

modules/sdk-coin-ada/src/lib/stakingActivateBuilder.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { TransactionBuilder } from './transactionBuilder';
44
import { Transaction } from './transaction';
55
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
6+
import adaUtils from './utils';
67

78
export class StakingActivateBuilder extends TransactionBuilder {
89
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -25,7 +26,7 @@ export class StakingActivateBuilder extends TransactionBuilder {
2526
* @param stakingPublicKey The user's public stake key
2627
* @param poolHash Pool ID Hash of the pool we are going to delegate to
2728
*/
28-
stakingCredential(stakingPublicKey: string, poolHash: string): this {
29+
stakingCredential(stakingPublicKey: string, poolHash: string, dRepId: string): this {
2930
const stakeCredential = CardanoWasm.Credential.from_keyhash(
3031
CardanoWasm.PublicKey.from_bytes(Buffer.from(stakingPublicKey, 'hex')).hash()
3132
);
@@ -40,6 +41,10 @@ export class StakingActivateBuilder extends TransactionBuilder {
4041
)
4142
);
4243
this._certs.push(stakeDelegationCert);
44+
const voteDelegationCert = CardanoWasm.Certificate.new_vote_delegation(
45+
CardanoWasm.VoteDelegation.new(stakeCredential, adaUtils.getDRepFromDRepId(dRepId))
46+
);
47+
this._certs.push(voteDelegationCert);
4348
return this;
4449
}
4550

modules/sdk-coin-ada/src/lib/transaction.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
1010
import { KeyPair } from './keyPair';
1111
import { BaseCoin as CoinConfig } from '@bitgo/statics';
12+
import adaUtils from './utils';
1213

1314
export interface TransactionInput {
1415
transaction_id: string;
@@ -31,17 +32,19 @@ export interface Witness {
3132
publicKey: string;
3233
signature: string;
3334
}
34-
enum CertType {
35+
export enum CertType {
3536
StakeKeyRegistration,
3637
StakeKeyDelegation,
3738
StakeKeyDeregistration,
3839
StakePoolRegistration,
40+
VoteDelegation,
3941
}
4042

4143
export interface Cert {
4244
type: CertType;
4345
stakeCredentialHash?: string;
4446
poolKeyHash?: string;
47+
dRepId?: string;
4548
}
4649

4750
export interface Withdrawal {
@@ -183,6 +186,14 @@ export class Transaction extends BaseTransaction {
183186
poolKeyHash: Buffer.from(stakePoolRegistration.pool_params().operator().to_bytes()).toString('hex'),
184187
});
185188
}
189+
if (cert.as_vote_delegation() !== undefined) {
190+
const voteDelegation = cert.as_vote_delegation() as CardanoWasm.VoteDelegation;
191+
result.certs.push({
192+
type: CertType.VoteDelegation,
193+
stakeCredentialHash: Buffer.from(voteDelegation.stake_credential().to_bytes()).toString('hex'),
194+
dRepId: adaUtils.getDRepIdFromDRep(voteDelegation.drep()),
195+
});
196+
}
186197
}
187198
}
188199

@@ -279,6 +290,8 @@ export class Transaction extends BaseTransaction {
279290
this._type = TransactionType.StakingActivate;
280291
} else if (certs.some((c) => c.as_stake_deregistration() !== undefined)) {
281292
this._type = TransactionType.StakingDeactivate;
293+
} else if (certs.some((c) => c.as_vote_delegation() !== undefined)) {
294+
this._type = TransactionType.VoteDelegation;
282295
}
283296
}
284297
if (this._transaction.body().withdrawals()) {
@@ -394,6 +407,8 @@ export class Transaction extends BaseTransaction {
394407
? 'StakingDeactivate'
395408
: this._type === TransactionType.StakingPledge
396409
? 'StakingPledge'
410+
: this._type === TransactionType.VoteDelegation
411+
? 'VoteDelegation'
397412
: 'undefined';
398413
return {
399414
displayOrder,

modules/sdk-coin-ada/src/lib/transactionBuilderFactory.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { StakingDeactivateBuilder } from './stakingDeactivateBuilder';
99
import { StakingWithdrawBuilder } from './stakingWithdrawBuilder';
1010
import { StakingPledgeBuilder } from './stakingPledgeBuilder';
1111
import { StakingClaimRewardsBuilder } from './stakingClaimRewardsBuilder';
12+
import { VoteDelegationBuilder } from './voteDelegationBuilder';
1213

1314
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1415
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -38,6 +39,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3839
return this.getWalletInitializationBuilder(tx);
3940
case TransactionType.StakingPledge:
4041
return this.getStakingPledgeBuilder(tx);
42+
case TransactionType.VoteDelegation:
43+
return this.getVoteDelegationBuilder(tx);
4144
default:
4245
throw new InvalidTransactionError('unsupported transaction');
4346
}
@@ -60,19 +63,23 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
6063
return TransactionBuilderFactory.initializeBuilder(tx, new StakingActivateBuilder(this._coinConfig));
6164
}
6265

63-
getStakingClaimRewardsBuilder(tx?: Transaction) {
66+
getVoteDelegationBuilder(tx?: Transaction): VoteDelegationBuilder {
67+
return TransactionBuilderFactory.initializeBuilder(tx, new VoteDelegationBuilder(this._coinConfig));
68+
}
69+
70+
getStakingClaimRewardsBuilder(tx?: Transaction): StakingClaimRewardsBuilder {
6471
return TransactionBuilderFactory.initializeBuilder(tx, new StakingClaimRewardsBuilder(this._coinConfig));
6572
}
6673

67-
getStakingDeactivateBuilder(tx?: Transaction) {
74+
getStakingDeactivateBuilder(tx?: Transaction): StakingDeactivateBuilder {
6875
return TransactionBuilderFactory.initializeBuilder(tx, new StakingDeactivateBuilder(this._coinConfig));
6976
}
7077

71-
getStakingWithdrawBuilder(tx?: Transaction) {
78+
getStakingWithdrawBuilder(tx?: Transaction): StakingWithdrawBuilder {
7279
return TransactionBuilderFactory.initializeBuilder(tx, new StakingWithdrawBuilder(this._coinConfig));
7380
}
7481

75-
getStakingPledgeBuilder(tx?: Transaction) {
82+
getStakingPledgeBuilder(tx?: Transaction): StakingPledgeBuilder {
7683
return TransactionBuilderFactory.initializeBuilder(tx, new StakingPledgeBuilder(this._coinConfig));
7784
}
7885

modules/sdk-coin-ada/src/lib/utils.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import {
77
Credential,
88
RewardAddress,
99
Transaction as CardanoTransaction,
10+
DRep,
11+
Ed25519KeyHash,
12+
ScriptHash,
13+
DRepKind,
1014
} from '@emurgo/cardano-serialization-lib-nodejs';
1115
import { KeyPair } from './keyPair';
1216
import { bech32 } from 'bech32';
1317

1418
export const MIN_ADA_FOR_ONE_ASSET = '1500000';
19+
export const VOTE_ALWAYS_ABSTAIN = 'always-abstain';
20+
export const VOTE_ALWAYS_NO_CONFIDENCE = 'always-no-confidence';
1521

1622
export class Utils implements BaseUtils {
1723
createBaseAddressWithStakeAndPaymentKey(
@@ -75,6 +81,65 @@ export class Utils implements BaseUtils {
7581
return rewardAddress.to_address().to_bech32();
7682
}
7783

84+
isValidDRepId(dRepId: string): boolean {
85+
try {
86+
this.getDRepFromDRepId(dRepId);
87+
return true;
88+
} catch (err) {
89+
return false;
90+
}
91+
}
92+
93+
getDRepFromDRepId(dRepId: string): DRep {
94+
switch (dRepId) {
95+
case 'always-abstain':
96+
return DRep.new_always_abstain();
97+
case 'always-no-confidence':
98+
return DRep.new_always_no_confidence();
99+
default:
100+
try {
101+
// for parsing CIP-105 standard DRep ID
102+
return DRep.from_bech32(dRepId);
103+
} catch (err) {
104+
// for parsing CIP-129 standard DRep ID
105+
// https://cips.cardano.org/cip/CIP-0129
106+
const decodedBech32 = bech32.decode(dRepId);
107+
const decodedBytes = Buffer.from(bech32.fromWords(decodedBech32.words));
108+
const header = decodedBytes[0];
109+
const keyBytes = decodedBytes.subarray(1);
110+
111+
const keyType = (header & 0xf0) >> 4;
112+
const credentialType = header & 0x0f;
113+
114+
if (keyType !== 0x02) {
115+
throw new Error('Invalid key type for DRep');
116+
}
117+
118+
switch (credentialType) {
119+
case 0x02:
120+
const ed25519KeyHash = Ed25519KeyHash.from_bytes(keyBytes);
121+
return DRep.new_key_hash(ed25519KeyHash);
122+
case 0x03:
123+
const scriptHash = ScriptHash.from_bytes(keyBytes);
124+
return DRep.new_script_hash(scriptHash);
125+
default:
126+
throw new Error('Invalid credential type for DRep');
127+
}
128+
}
129+
}
130+
}
131+
132+
getDRepIdFromDRep(dRep: DRep): string {
133+
switch (dRep.kind()) {
134+
case DRepKind.AlwaysAbstain:
135+
return VOTE_ALWAYS_ABSTAIN;
136+
case DRepKind.AlwaysNoConfidence:
137+
return VOTE_ALWAYS_NO_CONFIDENCE;
138+
default:
139+
return dRep.to_bech32();
140+
}
141+
}
142+
78143
/** @inheritdoc */
79144
// this will validate both stake and payment addresses
80145
isValidAddress(address: string): boolean {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { BaseKey, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionBuilder } from './transactionBuilder';
4+
import { Transaction } from './transaction';
5+
import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs';
6+
import adaUtils from './utils';
7+
8+
export class VoteDelegationBuilder extends TransactionBuilder {
9+
constructor(_coinConfig: Readonly<CoinConfig>) {
10+
super(_coinConfig);
11+
this._type = TransactionType.VoteDelegation;
12+
}
13+
14+
protected get transactionType(): TransactionType {
15+
return TransactionType.VoteDelegation;
16+
}
17+
18+
/** @inheritdoc */
19+
initBuilder(tx: Transaction): void {
20+
super.initBuilder(tx);
21+
}
22+
23+
/**
24+
* Creates the proper certificates needed to delegate a user's vote to a given DRep
25+
*
26+
* @param stakingPublicKey The user's public stake key
27+
* @param dRepId The DRep ID of the DRep we will delegate vote to
28+
*/
29+
addVoteDelegationCertificate(stakingPublicKey: string, dRepId: string): this {
30+
const stakeCredential = CardanoWasm.Credential.from_keyhash(
31+
CardanoWasm.PublicKey.from_bytes(Buffer.from(stakingPublicKey, 'hex')).hash()
32+
);
33+
const voteDelegationCert = CardanoWasm.Certificate.new_vote_delegation(
34+
CardanoWasm.VoteDelegation.new(stakeCredential, adaUtils.getDRepFromDRepId(dRepId))
35+
);
36+
this._certs.push(voteDelegationCert);
37+
return this;
38+
}
39+
40+
/** @inheritdoc */
41+
protected async buildImplementation(): Promise<Transaction> {
42+
const tx = await super.buildImplementation();
43+
tx.setTransactionType(TransactionType.VoteDelegation);
44+
return tx;
45+
}
46+
47+
/** @inheritdoc */
48+
protected fromImplementation(rawTransaction: string): Transaction {
49+
return super.fromImplementation(rawTransaction);
50+
}
51+
52+
/** @inheritdoc */
53+
protected signImplementation(key: BaseKey): Transaction {
54+
return super.signImplementation(key);
55+
}
56+
}

modules/sdk-coin-ada/test/resources/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,16 @@ export const rawTx = {
141141
address: 'addr_test1vr8rakm66rcfv4fcxqykg5lf0yv7lsyk9mvapx369jpvtcgfcuk7f',
142142
value: '248329150',
143143
},
144+
unsignedVoteDelegationTx:
145+
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a003a76a7021a00029259031a2faf0800048183098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8200581c8b75035882d4165bea8000c4d3f2c123ae33c1d92a751a78135a2402a0f5f6',
146+
unsignedVoteDelegationTxBody:
147+
'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a003a76a7021a00029259031a2faf0800048183098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8200581c8b75035882d4165bea8000c4d3f2c123ae33c1d92a751a78135a2402',
148+
unsignedVoteDelegationTxHash: '6e80e3a2a78ee67f2c79070f37c5bde90845b636972a94cb7766af11d8d907db',
144149
unsignedStakingActiveTx:
145-
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001beca7021a000297d9031a2faf0800048282008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3a0f5f6',
150+
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001be677021a00029e09031a2faf0800048382008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b383098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8102a0f5f6',
146151
unsignedStakingActiveTxBody:
147-
'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001beca7021a000297d9031a2faf0800048282008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3',
148-
unsignedStakingActiveTxHash: '4e2777152ebc92c73daebc0075cee8a52ca9fb8c00511ac2c035b58c52f27125',
152+
'a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a001be677021a00029e09031a2faf0800048382008200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb83028200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb581c7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b383098200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdb8102',
153+
unsignedStakingActiveTxHash: 'd8ecaa7bfee2e28691673be378ea6583bc8efd5a6e29ca9cfb278279a06dd216',
149154
unsignedStakingDeactiveTx:
150155
'84a500818258203677e75c7ba699bfdc6cd57d42f246f86f63aefd76025006ac78313fad2bba2101018182583901c7b28bcea90d440b5455a6a02a27ca59b8696f067fc1967f47f933e79558e969caa9e57adcfc40b9907eb794363b590faf42fff48c38eb881a005900a7021a00028cd9031a2faf0800048182018200581c188fde65b1f9bd69b0edcc4e5a65fa93a13773090e1e2eef7a25cfdba0f5f6',
151156
unsignedStakingDeactiveTxBody:

modules/sdk-coin-ada/test/unit/StakingActivateBuilder.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { TransactionType, AddressFormat } from '@bitgo/sdk-core';
33
import * as testData from '../resources';
44
import { KeyPair, TransactionBuilderFactory } from '../../src';
55
import { coins } from '@bitgo/statics';
6-
import { Transaction } from '../../src/lib/transaction';
6+
import { CertType, Transaction } from '../../src/lib/transaction';
77
import * as Utils from '../../src/lib/utils';
88

99
describe('ADA Staking Activate Transaction Builder', async () => {
@@ -21,19 +21,24 @@ describe('ADA Staking Activate Transaction Builder', async () => {
2121
'addr1q8rm9z7w4yx5gz652kn2q238efvms6t0qelur9nlglun8eu4tr5knj4fu4adelzqhxg8adu5xca4jra0gtllfrpcawyq9psz23',
2222
totalInput
2323
);
24-
txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3');
24+
txBuilder.stakingCredential(
25+
keyPairStake.getKeys().pub,
26+
'7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3',
27+
'always-abstain'
28+
);
2529
txBuilder.ttl(800000000);
2630
const tx = (await txBuilder.build()) as Transaction;
2731
should.equal(tx.type, TransactionType.StakingActivate);
2832
const txData = tx.toJson();
2933
const fee = tx.getFee;
30-
txData.certs.length.should.equal(2);
34+
txData.certs.length.should.equal(3);
3135
txData.certs[0].type.should.equal(0);
3236
txData.certs[1].type.should.equal(1);
37+
txData.certs[2].type.should.equal(CertType.VoteDelegation);
3338

3439
txData.outputs.length.should.equal(1);
3540
txData.outputs[0].amount.should.equal((Number(totalInput) - 2000000 - Number(fee)).toString());
36-
fee.should.equal('169945');
41+
fee.should.equal('171529');
3742
tx.toBroadcastFormat().should.equal(testData.rawTx.unsignedStakingActiveTx);
3843
should.equal(tx.id, testData.rawTx.unsignedStakingActiveTxHash);
3944
});
@@ -50,7 +55,11 @@ describe('ADA Staking Activate Transaction Builder', async () => {
5055
'addr1q8rm9z7w4yx5gz652kn2q238efvms6t0qelur9nlglun8eu4tr5knj4fu4adelzqhxg8adu5xca4jra0gtllfrpcawyq9psz23',
5156
totalInput
5257
);
53-
txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3');
58+
txBuilder.stakingCredential(
59+
keyPairStake.getKeys().pub,
60+
'7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3',
61+
'always-abstain'
62+
);
5463
txBuilder.ttl(800000000);
5564
const tx = (await txBuilder.build()) as Transaction;
5665
should.equal(tx.type, TransactionType.StakingActivate);
@@ -129,7 +138,11 @@ describe('ADA Staking Activate Transaction Builder', async () => {
129138
const txBuilder = factory.getStakingActivateBuilder();
130139
const senderBalance = '22122071';
131140
txBuilder.changeAddress(senderAddress, senderBalance);
132-
txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3');
141+
txBuilder.stakingCredential(
142+
keyPairStake.getKeys().pub,
143+
'7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3',
144+
'always-no-confidence'
145+
);
133146

134147
txBuilder.input({
135148
transaction_id: '0a4f80d83ba9ce1f83306a79252909241308d7eff317d04c9ea018966d687fe3',
@@ -172,7 +185,11 @@ describe('ADA Staking Activate Transaction Builder', async () => {
172185
const txBuilder = factory.getStakingActivateBuilder();
173186
const senderBalance = '22122071';
174187
txBuilder.changeAddress(senderAddress, senderBalance);
175-
txBuilder.stakingCredential(keyPairStake.getKeys().pub, '7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3');
188+
txBuilder.stakingCredential(
189+
keyPairStake.getKeys().pub,
190+
'7a623c48348501c2380e60ac2307fcd1b67df4218f819930821a15b3',
191+
'always-no-confidence'
192+
);
176193

177194
txBuilder.input({
178195
transaction_id: '0a4f80d83ba9ce1f83306a79252909241308d7eff317d04c9ea018966d687fe3',

0 commit comments

Comments
 (0)