Skip to content

Commit ab838bf

Browse files
committed
feat(sdk-coin-canton): add cosign delegation accept builder'
Ticket: CHALO-438
1 parent c1af298 commit ab838bf

8 files changed

Lines changed: 230 additions & 0 deletions

File tree

modules/sdk-coin-canton/src/canton.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export class Canton extends BaseCoin {
114114
case TransactionType.TransferReject:
115115
case TransactionType.TransferAcknowledge:
116116
case TransactionType.TransferOfferWithdrawn:
117+
case TransactionType.CosignDelegationAccept:
118+
case TransactionType.CosignDelegationProposal:
117119
// There is no input for these type of transactions, so always return true.
118120
return true;
119121
case TransactionType.OneStepPreApproval:
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { CantonPrepareCommandResponse, CantonTransferAcceptRejectRequest } from './iface';
4+
import { TransactionBuilder } from './transactionBuilder';
5+
import { Transaction } from './transaction/transaction';
6+
import utils from './utils';
7+
8+
export class CosignDelegationAcceptBuilder extends TransactionBuilder {
9+
private _commandId: string;
10+
private _contractId: string;
11+
private _actAsPartyId: string;
12+
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
}
16+
17+
initBuilder(tx: Transaction): void {
18+
super.initBuilder(tx);
19+
this.setTransactionType();
20+
}
21+
22+
get transactionType(): TransactionType {
23+
return TransactionType.CosignDelegationAccept;
24+
}
25+
26+
setTransactionType(): void {
27+
this.transaction.transactionType = TransactionType.CosignDelegationAccept;
28+
}
29+
30+
setTransaction(transaction: CantonPrepareCommandResponse): void {
31+
this.transaction.prepareCommand = transaction;
32+
}
33+
34+
/** @inheritDoc */
35+
addSignature(publicKey: PublicKey, signature: Buffer): void {
36+
if (!this.transaction) {
37+
throw new InvalidTransactionError('transaction is empty!');
38+
}
39+
this._signatures.push({ publicKey, signature });
40+
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
41+
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
42+
this.transaction.signatures = signature.toString('base64');
43+
}
44+
45+
/**
46+
* Sets the unique command id for the cosign delegation accept
47+
* Also sets the _id of the transaction
48+
*
49+
* @param id - A uuid
50+
* @returns The current builder instance for chaining.
51+
* @throws Error if id is empty.
52+
*/
53+
commandId(id: string): this {
54+
if (!id || !id.trim()) {
55+
throw new Error('commandId must be a non-empty string');
56+
}
57+
this._commandId = id.trim();
58+
this.transaction.id = id.trim();
59+
return this;
60+
}
61+
62+
/**
63+
* Sets the contract id of the delegation proposal to accept
64+
* @param id - canton contract id
65+
* @returns The current builder instance for chaining.
66+
* @throws Error if id is empty.
67+
*/
68+
contractId(id: string): this {
69+
if (!id || !id.trim()) {
70+
throw new Error('contractId must be a non-empty string');
71+
}
72+
this._contractId = id.trim();
73+
return this;
74+
}
75+
76+
/**
77+
* Sets the party acting as the acceptor
78+
*
79+
* @param id - the actor party id (address)
80+
* @returns The current builder instance for chaining.
81+
* @throws Error if id is empty.
82+
*/
83+
actAs(id: string): this {
84+
if (!id || !id.trim()) {
85+
throw new Error('actAsPartyId must be a non-empty string');
86+
}
87+
this._actAsPartyId = id.trim();
88+
return this;
89+
}
90+
91+
/**
92+
* Builds and returns the CantonTransferAcceptRejectRequest object from the builder's internal state.
93+
*
94+
* @returns {CantonTransferAcceptRejectRequest} - A fully constructed and validated request object.
95+
* @throws {Error} If any required field is missing or fails validation.
96+
*/
97+
toRequestObject(): CantonTransferAcceptRejectRequest {
98+
this.validate();
99+
100+
return {
101+
commandId: this._commandId,
102+
contractId: this._contractId,
103+
verboseHashing: false,
104+
actAs: [this._actAsPartyId],
105+
readAs: [],
106+
};
107+
}
108+
109+
/**
110+
* Validates the internal state of the builder before building the request object.
111+
*
112+
* @private
113+
* @throws {Error} If any required field is missing or invalid.
114+
*/
115+
private validate(): void {
116+
if (!this._commandId) throw new Error('commandId is missing');
117+
if (!this._contractId) throw new Error('contractId is missing');
118+
if (!this._actAsPartyId) throw new Error('actAs partyId is missing');
119+
}
120+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as Utils from './utils';
22
import * as Interface from './iface';
33

4+
export { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder';
45
export { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
56
export { KeyPair } from './keyPair';
67
export { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export class Transaction extends BaseTransaction {
269269
let inputAmount = '0';
270270
let outputAmount = '0';
271271
switch (this.type) {
272+
case TransactionType.CosignDelegationAccept:
272273
case TransactionType.TransferAccept:
273274
case TransactionType.TransferReject: {
274275
const txData = this.toJson();

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
TransactionType,
66
} from '@bitgo/sdk-core';
77
import { BaseCoin as CoinConfig } from '@bitgo/statics';
8+
import { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder';
89
import { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
910
import { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
1011
import { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
@@ -46,6 +47,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
4647
case TransactionType.CosignDelegationProposal: {
4748
return this.getCosignDelegationProposalBuilder(tx);
4849
}
50+
case TransactionType.CosignDelegationAccept: {
51+
return this.getCosignDelegationAcceptBuilder(tx);
52+
}
4953
case TransactionType.TransferOfferWithdrawn: {
5054
return this.getTransferOfferWithdrawnBuilder(tx);
5155
}
@@ -75,6 +79,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
7579
return TransactionBuilderFactory.initializeBuilder(tx, new CosignDelegationProposalBuilder(this._coinConfig));
7680
}
7781

82+
getCosignDelegationAcceptBuilder(tx?: Transaction): CosignDelegationAcceptBuilder {
83+
return TransactionBuilderFactory.initializeBuilder(tx, new CosignDelegationAcceptBuilder(this._coinConfig));
84+
}
85+
7886
getTransferOfferWithdrawnBuilder(tx?: Transaction): TransferOfferWithdrawnBuilder {
7987
return TransactionBuilderFactory.initializeBuilder(tx, new TransferOfferWithdrawnBuilder(this._coinConfig));
8088
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,20 @@ export class Utils implements BaseUtils {
308308
break;
309309
}
310310

311+
case TransactionType.CosignDelegationAccept: {
312+
// exercise CosignDelegationProposal_Accept → actingParties[0] = signer (sender)
313+
const signerParty = findExerciseActingParty('CosignDelegationProposal_Accept');
314+
if (signerParty) sender = signerParty;
315+
// CosignDelegationProposal create node → admin = receiver
316+
const proposalFields = findCreateNodeFields('CosignDelegationProposal');
317+
if (proposalFields) {
318+
const adminData = getField(proposalFields, 'admin');
319+
if (adminData?.oneofKind === 'party') receiver = adminData.party ?? '';
320+
}
321+
amount = '0';
322+
break;
323+
}
324+
311325
case TransactionType.TransferOfferWithdrawn: {
312326
// Canton coin: Amulet create node → owner=sender=receiver, amount.initialAmount
313327
const amuletFields = findCreateNodeFields('Amulet');
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import assert from 'assert';
2+
import should from 'should';
3+
4+
import { coins } from '@bitgo/statics';
5+
6+
import { CosignDelegationAcceptBuilder, Transaction } from '../../../../src';
7+
import { CantonTransferAcceptRejectRequest } from '../../../../src/lib/iface';
8+
9+
const commandId = '3935a06d-3b03-41be-99a5-95b2ecaabf7d';
10+
const contractId =
11+
'001b549bfa833bab661ab30e4d0a3ab0ec01fcc4a2bef5369795f4928147706353ca1112205a8d0e780cf3b3115cf8be0d6315f4aed6a1c25b67e8c5d64cf9848d0458fd17';
12+
const actAsPartyId = '12205::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d';
13+
14+
describe('CosignDelegationAccept Builder', () => {
15+
it('should get the cosign delegation accept request object', function () {
16+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
17+
const tx = new Transaction(coins.get('tcanton'));
18+
txBuilder.initBuilder(tx);
19+
txBuilder.commandId(commandId).contractId(contractId).actAs(actAsPartyId);
20+
const requestObj: CantonTransferAcceptRejectRequest = txBuilder.toRequestObject();
21+
should.exist(requestObj);
22+
assert.equal(requestObj.commandId, commandId);
23+
assert.equal(requestObj.contractId, contractId);
24+
assert.equal(requestObj.actAs.length, 1);
25+
assert.equal(requestObj.actAs[0], actAsPartyId);
26+
assert.deepEqual(requestObj.readAs, []);
27+
assert.equal(requestObj.verboseHashing, false);
28+
});
29+
30+
it('should set transaction id from commandId', function () {
31+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
32+
const tx = new Transaction(coins.get('tcanton'));
33+
txBuilder.initBuilder(tx);
34+
txBuilder.commandId(commandId);
35+
assert.equal(txBuilder.transaction.id, commandId);
36+
});
37+
38+
it('should throw if commandId is missing', function () {
39+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
40+
const tx = new Transaction(coins.get('tcanton'));
41+
txBuilder.initBuilder(tx);
42+
txBuilder.contractId(contractId).actAs(actAsPartyId);
43+
assert.throws(() => txBuilder.toRequestObject(), /commandId is missing/);
44+
});
45+
46+
it('should throw if contractId is missing', function () {
47+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
48+
const tx = new Transaction(coins.get('tcanton'));
49+
txBuilder.initBuilder(tx);
50+
txBuilder.commandId(commandId).actAs(actAsPartyId);
51+
assert.throws(() => txBuilder.toRequestObject(), /contractId is missing/);
52+
});
53+
54+
it('should throw if actAs is missing', function () {
55+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
56+
const tx = new Transaction(coins.get('tcanton'));
57+
txBuilder.initBuilder(tx);
58+
txBuilder.commandId(commandId).contractId(contractId);
59+
assert.throws(() => txBuilder.toRequestObject(), /actAs partyId is missing/);
60+
});
61+
62+
it('should throw if commandId is empty string', function () {
63+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
64+
const tx = new Transaction(coins.get('tcanton'));
65+
txBuilder.initBuilder(tx);
66+
assert.throws(() => txBuilder.commandId(''), /commandId must be a non-empty string/);
67+
});
68+
69+
it('should throw if contractId is empty string', function () {
70+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
71+
const tx = new Transaction(coins.get('tcanton'));
72+
txBuilder.initBuilder(tx);
73+
assert.throws(() => txBuilder.contractId(''), /contractId must be a non-empty string/);
74+
});
75+
76+
it('should throw if actAs is empty string', function () {
77+
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
78+
const tx = new Transaction(coins.get('tcanton'));
79+
txBuilder.initBuilder(tx);
80+
assert.throws(() => txBuilder.actAs(''), /actAsPartyId must be a non-empty string/);
81+
});
82+
});

modules/sdk-core/src/account-lib/baseCoin/enum.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ export enum TransactionType {
9999
TransferOfferWithdrawn,
100100
// canton cosign delegation proposal
101101
CosignDelegationProposal,
102+
// canton cosign delegation accept
103+
CosignDelegationAccept,
102104

103105
// trx
104106
FREEZE,

0 commit comments

Comments
 (0)