Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export class Canton extends BaseCoin {
case TransactionType.TransferReject:
case TransactionType.TransferAcknowledge:
case TransactionType.TransferOfferWithdrawn:
case TransactionType.CosignDelegationAccept:
case TransactionType.CosignDelegationProposal:
// There is no input for these type of transactions, so always return true.
return true;
case TransactionType.OneStepPreApproval:
Expand Down
120 changes: 120 additions & 0 deletions modules/sdk-coin-canton/src/lib/cosignDelegationAcceptBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CantonPrepareCommandResponse, CantonTransferAcceptRejectRequest } from './iface';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction/transaction';
import utils from './utils';

export class CosignDelegationAcceptBuilder extends TransactionBuilder {
private _commandId: string;
private _contractId: string;
private _actAsPartyId: string;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.setTransactionType();
}

get transactionType(): TransactionType {
return TransactionType.CosignDelegationAccept;
}

setTransactionType(): void {
this.transaction.transactionType = TransactionType.CosignDelegationAccept;
}

setTransaction(transaction: CantonPrepareCommandResponse): void {
this.transaction.prepareCommand = transaction;
}

/** @inheritDoc */
addSignature(publicKey: PublicKey, signature: Buffer): void {
if (!this.transaction) {
throw new InvalidTransactionError('transaction is empty!');
}
this._signatures.push({ publicKey, signature });
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
this.transaction.signatures = signature.toString('base64');
}

/**
* Sets the unique command id for the cosign delegation accept
* Also sets the _id of the transaction
*
* @param id - A uuid
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
commandId(id: string): this {
if (!id || !id.trim()) {
throw new Error('commandId must be a non-empty string');
}
this._commandId = id.trim();
this.transaction.id = id.trim();
return this;
}

/**
* Sets the contract id of the delegation proposal to accept
* @param id - canton contract id
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
contractId(id: string): this {
if (!id || !id.trim()) {
throw new Error('contractId must be a non-empty string');
}
this._contractId = id.trim();
return this;
}

/**
* Sets the party acting as the acceptor
*
* @param id - the actor party id (address)
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
actAs(id: string): this {
if (!id || !id.trim()) {
throw new Error('actAsPartyId must be a non-empty string');
}
this._actAsPartyId = id.trim();
return this;
}

/**
* Builds and returns the CantonTransferAcceptRejectRequest object from the builder's internal state.
*
* @returns {CantonTransferAcceptRejectRequest} - A fully constructed and validated request object.
* @throws {Error} If any required field is missing or fails validation.
*/
toRequestObject(): CantonTransferAcceptRejectRequest {
this.validate();

return {
commandId: this._commandId,
contractId: this._contractId,
verboseHashing: false,
actAs: [this._actAsPartyId],
readAs: [],
};
}

/**
* Validates the internal state of the builder before building the request object.
*
* @private
* @throws {Error} If any required field is missing or invalid.
*/
private validate(): void {
if (!this._commandId) throw new Error('commandId is missing');
if (!this._contractId) throw new Error('contractId is missing');
if (!this._actAsPartyId) throw new Error('actAs partyId is missing');
}
}
1 change: 1 addition & 0 deletions modules/sdk-coin-canton/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Utils from './utils';
import * as Interface from './iface';

export { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder';
export { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
export { KeyPair } from './keyPair';
export { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export class Transaction extends BaseTransaction {
let inputAmount = '0';
let outputAmount = '0';
switch (this.type) {
case TransactionType.CosignDelegationAccept:
case TransactionType.TransferAccept:
case TransactionType.TransferReject: {
const txData = this.toJson();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder';
import { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
import { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
import { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
Expand Down Expand Up @@ -46,6 +47,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
case TransactionType.CosignDelegationProposal: {
return this.getCosignDelegationProposalBuilder(tx);
}
case TransactionType.CosignDelegationAccept: {
return this.getCosignDelegationAcceptBuilder(tx);
}
case TransactionType.TransferOfferWithdrawn: {
return this.getTransferOfferWithdrawnBuilder(tx);
}
Expand Down Expand Up @@ -75,6 +79,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return TransactionBuilderFactory.initializeBuilder(tx, new CosignDelegationProposalBuilder(this._coinConfig));
}

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

getTransferOfferWithdrawnBuilder(tx?: Transaction): TransferOfferWithdrawnBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new TransferOfferWithdrawnBuilder(this._coinConfig));
}
Expand Down
14 changes: 14 additions & 0 deletions modules/sdk-coin-canton/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,20 @@ export class Utils implements BaseUtils {
break;
}

case TransactionType.CosignDelegationAccept: {
// exercise CosignDelegationProposal_Accept → actingParties[0] = signer (sender)
const signerParty = findExerciseActingParty('CosignDelegationProposal_Accept');
if (signerParty) sender = signerParty;
// CosignDelegationProposal create node → admin = receiver
const proposalFields = findCreateNodeFields('CosignDelegationProposal');
if (proposalFields) {
const adminData = getField(proposalFields, 'admin');
if (adminData?.oneofKind === 'party') receiver = adminData.party ?? '';
}
amount = '0';
break;
}

case TransactionType.TransferOfferWithdrawn: {
// Canton coin: Amulet create node → owner=sender=receiver, amount.initialAmount
const amuletFields = findCreateNodeFields('Amulet');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import assert from 'assert';
import should from 'should';

import { coins } from '@bitgo/statics';

import { CosignDelegationAcceptBuilder, Transaction } from '../../../../src';
import { CantonTransferAcceptRejectRequest } from '../../../../src/lib/iface';

const commandId = '3935a06d-3b03-41be-99a5-95b2ecaabf7d';
const contractId =
'001b549bfa833bab661ab30e4d0a3ab0ec01fcc4a2bef5369795f4928147706353ca1112205a8d0e780cf3b3115cf8be0d6315f4aed6a1c25b67e8c5d64cf9848d0458fd17';
const actAsPartyId = '12205::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d';

describe('CosignDelegationAccept Builder', () => {
it('should get the cosign delegation accept request object', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
txBuilder.commandId(commandId).contractId(contractId).actAs(actAsPartyId);
const requestObj: CantonTransferAcceptRejectRequest = txBuilder.toRequestObject();
should.exist(requestObj);
assert.equal(requestObj.commandId, commandId);
assert.equal(requestObj.contractId, contractId);
assert.equal(requestObj.actAs.length, 1);
assert.equal(requestObj.actAs[0], actAsPartyId);
assert.deepEqual(requestObj.readAs, []);
assert.equal(requestObj.verboseHashing, false);
});

it('should set transaction id from commandId', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
txBuilder.commandId(commandId);
assert.equal(txBuilder.transaction.id, commandId);
});

it('should throw if commandId is missing', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
txBuilder.contractId(contractId).actAs(actAsPartyId);
assert.throws(() => txBuilder.toRequestObject(), /commandId is missing/);
});

it('should throw if contractId is missing', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
txBuilder.commandId(commandId).actAs(actAsPartyId);
assert.throws(() => txBuilder.toRequestObject(), /contractId is missing/);
});

it('should throw if actAs is missing', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
txBuilder.commandId(commandId).contractId(contractId);
assert.throws(() => txBuilder.toRequestObject(), /actAs partyId is missing/);
});

it('should throw if commandId is empty string', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
assert.throws(() => txBuilder.commandId(''), /commandId must be a non-empty string/);
});

it('should throw if contractId is empty string', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
assert.throws(() => txBuilder.contractId(''), /contractId must be a non-empty string/);
});

it('should throw if actAs is empty string', function () {
const txBuilder = new CosignDelegationAcceptBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(tx);
assert.throws(() => txBuilder.actAs(''), /actAsPartyId must be a non-empty string/);
});
});
2 changes: 2 additions & 0 deletions modules/sdk-core/src/account-lib/baseCoin/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export enum TransactionType {
TransferOfferWithdrawn,
// canton cosign delegation proposal
CosignDelegationProposal,
// canton cosign delegation accept
CosignDelegationAccept,

// trx
FREEZE,
Expand Down
Loading