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
105 changes: 105 additions & 0 deletions modules/sdk-coin-canton/src/lib/cosignDelegationProposalBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { PublicKey, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CantonPrepareCommandResponse, CosignDelegationProposal } from './iface';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction/transaction';

export class CosignDelegationProposalBuilder extends TransactionBuilder {
private _contractId: string;
private _operatorId: string;
private _packageName?: string;

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

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

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

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

setTransaction(transaction: CantonPrepareCommandResponse): void {
throw new Error('Not implemented!');
}

/** @inheritDoc */
addSignature(publicKey: PublicKey, signature: Buffer): void {
throw new Error('Not implemented!');
}

/**
* Sets the contract id of the delegation proposal to cosign
* @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 operator party id
* @param id - operator party id
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
operatorId(id: string): this {
if (!id || !id.trim()) {
throw new Error('operatorId must be a non-empty string');
}
this._operatorId = id.trim();
return this;
}

/**
* Sets the optional package name
* @param name - package name
* @returns The current builder instance for chaining.
*/
packageName(name: string): this {
this._packageName = name;
return this;
}

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

const result: CosignDelegationProposal = {
contractId: this._contractId,
operatorId: this._operatorId,
};
if (this._packageName !== undefined) {
result.packageName = this._packageName;
}
return result;
}

/**
* 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._contractId) throw new Error('contractId is missing');
if (!this._operatorId) throw new Error('operatorId is missing');
}
}
8 changes: 8 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface TxData {
receiver: string;
amount: string;
acknowledgeData?: TransferAcknowledge;
cosignDelegationProposalData?: CosignDelegationProposal;
memoId?: string;
token?: string;
}
Expand Down Expand Up @@ -112,6 +113,7 @@ export interface PartySignature {

export interface TransactionBroadcastData {
acknowledgeData?: TransferAcknowledge;
cosignDelegationProposalData?: CosignDelegationProposal;
prepareCommandResponse?: CantonPrepareCommandResponse;
txType: string;
preparedTransaction?: string;
Expand Down Expand Up @@ -151,6 +153,12 @@ export interface TransferAcknowledge {
updateId: string;
}

export interface CosignDelegationProposal {
contractId: string;
operatorId: string;
packageName?: string;
}

export interface CantonTransferRequest {
commandId: string;
senderPartyId: string;
Expand Down
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 { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
export { KeyPair } from './keyPair';
export { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
export { Transaction } from './transaction/transaction';
Expand Down
40 changes: 34 additions & 6 deletions modules/sdk-coin-canton/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import {
CantonPrepareCommandResponse,
CosignDelegationProposal,
MultiHashSignature,
PartySignature,
PreparedTxnParsedInfo,
Expand All @@ -24,6 +25,7 @@ export class Transaction extends BaseTransaction {
private _prepareCommand: CantonPrepareCommandResponse;
private _signerFingerprint: string;
private _acknowledgeData: TransferAcknowledge;
private _cosignDelegationProposalData: CosignDelegationProposal;

constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
Expand All @@ -45,6 +47,10 @@ export class Transaction extends BaseTransaction {
this._acknowledgeData = data;
}

set cosignDelegationProposalData(data: CosignDelegationProposal) {
this._cosignDelegationProposalData = data;
}

get id(): string {
if (!this._id) {
throw new InvalidTransactionError('transaction is is not set');
Expand Down Expand Up @@ -83,6 +89,17 @@ export class Transaction extends BaseTransaction {
};
return Buffer.from(JSON.stringify(minData)).toString('base64');
}
if (this._type === TransactionType.CosignDelegationProposal) {
if (!this._cosignDelegationProposalData) {
throw new InvalidTransactionError('CosignDelegationProposalData is not set');
}
const minData: TransactionBroadcastData = {
txType: TransactionType[this._type],
submissionId: this.id,
cosignDelegationProposalData: this._cosignDelegationProposalData,
};
return Buffer.from(JSON.stringify(minData)).toString('base64');
}
if (!this._prepareCommand) {
throw new InvalidTransactionError('Empty transaction data');
}
Expand Down Expand Up @@ -144,6 +161,13 @@ export class Transaction extends BaseTransaction {
result.acknowledgeData = this._acknowledgeData;
return result;
}
if (this._type === TransactionType.CosignDelegationProposal) {
if (!this._cosignDelegationProposalData) {
throw new InvalidTransactionError('CosignDelegationProposalData is not set');
}
result.cosignDelegationProposalData = this._cosignDelegationProposalData;
return result;
}
if (!this._prepareCommand || !this._prepareCommand.preparedTransaction) {
throw new InvalidTransactionError('Empty transaction data');
}
Expand All @@ -167,7 +191,7 @@ export class Transaction extends BaseTransaction {
}

get signablePayload(): Buffer {
if (this._type === TransactionType.TransferAcknowledge) {
if (this._type === TransactionType.TransferAcknowledge || this._type === TransactionType.CosignDelegationProposal) {
return Buffer.from(DUMMY_HASH, 'base64');
}
if (!this._prepareCommand) {
Expand All @@ -181,7 +205,15 @@ export class Transaction extends BaseTransaction {
const decoded: TransactionBroadcastData = JSON.parse(Buffer.from(rawTx, 'base64').toString('utf8'));
this.id = decoded.submissionId;
this.transactionType = TransactionType[decoded.txType];
if (this.type !== TransactionType.TransferAcknowledge) {
if (this.type === TransactionType.TransferAcknowledge) {
if (decoded.acknowledgeData) {
this.acknowledgeData = decoded.acknowledgeData;
}
} else if (this.type === TransactionType.CosignDelegationProposal) {
if (decoded.cosignDelegationProposalData) {
this.cosignDelegationProposalData = decoded.cosignDelegationProposalData;
}
} else {
if (decoded.prepareCommandResponse) {
this.prepareCommand = decoded.prepareCommandResponse;
this.loadInputsAndOutputs();
Expand All @@ -190,10 +222,6 @@ export class Transaction extends BaseTransaction {
this.signerFingerprint = decoded.partySignatures.signatures[0].party.split('::')[1];
this.signatures = decoded.partySignatures.signatures[0].signatures[0].signature;
}
} else {
if (decoded.acknowledgeData) {
this.acknowledgeData = decoded.acknowledgeData;
}
}
} catch (e) {
throw new InvalidTransactionError('Unable to parse raw transaction data');
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 { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
import { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
import { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
import { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
Expand Down Expand Up @@ -42,6 +43,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
case TransactionType.TransferAcknowledge: {
return this.getTransferAcknowledgeBuilder(tx);
}
case TransactionType.CosignDelegationProposal: {
return this.getCosignDelegationProposalBuilder(tx);
}
case TransactionType.TransferOfferWithdrawn: {
return this.getTransferOfferWithdrawnBuilder(tx);
}
Expand All @@ -67,6 +71,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return TransactionBuilderFactory.initializeBuilder(tx, new TransferAcknowledgeBuilder(this._coinConfig));
}

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

getTransferOfferWithdrawnBuilder(tx?: Transaction): TransferOfferWithdrawnBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new TransferOfferWithdrawnBuilder(this._coinConfig));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import assert from 'assert';
import should from 'should';

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

import { CosignDelegationProposalBuilder, Transaction } from '../../../../src';
import { CosignDelegationProposal } from '../../../../src/lib/iface';

const contractId =
'001b549bfa833bab661ab30e4d0a3ab0ec01fcc4a2bef5369795f4928147706353ca1112205a8d0e780cf3b3115cf8be0d6315f4aed6a1c25b67e8c5d64cf9848d0458fd17';
const operatorId = '12205::12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d';
const packageName = 'splice-amulet';
const submissionId = '12205b4e3537a95126d90604592344d8ad3c3ddccda4f79901954280ee19c576714d';

describe('CosignDelegationProposal Builder', () => {
it('should get the cosign delegation proposal request object', function () {
const txBuilder = new CosignDelegationProposalBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
tx.id = submissionId;
txBuilder.initBuilder(tx);
txBuilder.contractId(contractId).operatorId(operatorId).packageName(packageName);
const requestObj: CosignDelegationProposal = txBuilder.toRequestObject();
should.exist(requestObj);
assert.equal(requestObj.contractId, contractId);
assert.equal(requestObj.operatorId, operatorId);
assert.equal(requestObj.packageName, packageName);
});

it('should get the cosign delegation proposal request object without packageName', function () {
const txBuilder = new CosignDelegationProposalBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
tx.id = submissionId;
txBuilder.initBuilder(tx);
txBuilder.contractId(contractId).operatorId(operatorId);
const requestObj: CosignDelegationProposal = txBuilder.toRequestObject();
should.exist(requestObj);
assert.equal(requestObj.contractId, contractId);
assert.equal(requestObj.operatorId, operatorId);
assert.equal(requestObj.packageName, undefined);
});

it('should throw if contractId is missing', function () {
const txBuilder = new CosignDelegationProposalBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
tx.id = submissionId;
txBuilder.initBuilder(tx);
txBuilder.operatorId(operatorId);
assert.throws(() => txBuilder.toRequestObject(), /contractId is missing/);
});

it('should throw if operatorId is missing', function () {
const txBuilder = new CosignDelegationProposalBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
tx.id = submissionId;
txBuilder.initBuilder(tx);
txBuilder.contractId(contractId);
assert.throws(() => txBuilder.toRequestObject(), /operatorId is missing/);
});

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

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

it('should throw on setTransaction', function () {
const txBuilder = new CosignDelegationProposalBuilder(coins.get('tcanton'));
assert.throws(() => txBuilder.setTransaction({} as any), /Not implemented/);
});

it('should throw on addSignature', function () {
const txBuilder = new CosignDelegationProposalBuilder(coins.get('tcanton'));
assert.throws(() => txBuilder.addSignature({} as any, Buffer.from('')), /Not implemented/);
});
});
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 @@ -97,6 +97,8 @@ export enum TransactionType {
TransferReject,
// canton transfer offer withdrawn, 2-step
TransferOfferWithdrawn,
// canton cosign delegation proposal
CosignDelegationProposal,

// trx
FREEZE,
Expand Down
Loading