Skip to content

Commit 48b7c67

Browse files
authored
Merge pull request #7358 from BitGo/COIN-6019
feat: added transfer reject builder
2 parents 1b4af78 + ac4fbe7 commit 48b7c67

File tree

9 files changed

+216
-24
lines changed

9 files changed

+216
-24
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export interface CantonOneStepEnablementRequest extends CantonPrepareCommandRequ
111111
receiverId: string;
112112
}
113113

114-
export interface CantonTransferAcceptRequest extends CantonPrepareCommandRequest {
114+
export interface CantonTransferAcceptRejectRequest extends CantonPrepareCommandRequest {
115115
contractId: string;
116116
}
117117

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
88
export { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
99
export { TransactionBuilder } from './transactionBuilder';
1010
export { TransactionBuilderFactory } from './transactionBuilderFactory';
11+
export { TransferRejectionBuilder } from './transferRejectionBuilder';
1112
export { WalletInitBuilder } from './walletInitBuilder';
1213
export { WalletInitTransaction } from './walletInitialization/walletInitTransaction';
1314

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
99
import { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
1010
import { TransactionBuilder } from './transactionBuilder';
1111
import { TransferBuilder } from './transferBuilder';
12+
import { TransferRejectionBuilder } from './transferRejectionBuilder';
1213
import { Transaction } from './transaction/transaction';
1314
import { WalletInitBuilder } from './walletInitBuilder';
1415
import { WalletInitTransaction } from './walletInitialization/walletInitTransaction';
@@ -36,6 +37,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3637
case TransactionType.TransferAcknowledge: {
3738
return this.getTransferAcknowledgeBuilder(tx);
3839
}
40+
case TransactionType.TransferReject: {
41+
return this.getTransferRejectBuilder(tx);
42+
}
3943
default: {
4044
throw new InvalidTransactionError('unsupported transaction');
4145
}
@@ -51,6 +55,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
5155
return TransactionBuilderFactory.initializeBuilder(tx, new TransferAcknowledgeBuilder(this._coinConfig));
5256
}
5357

58+
getTransferRejectBuilder(tx?: Transaction): TransferRejectionBuilder {
59+
return TransactionBuilderFactory.initializeBuilder(tx, new TransferRejectionBuilder(this._coinConfig));
60+
}
61+
5462
/** @inheritdoc */
5563
getTransferBuilder(tx?: Transaction): TransferBuilder {
5664
return TransactionBuilderFactory.initializeBuilder(tx, new TransferBuilder(this._coinConfig));

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3-
import { CantonPrepareCommandResponse, CantonTransferAcceptRequest } from './iface';
3+
import { CantonPrepareCommandResponse, CantonTransferAcceptRejectRequest } from './iface';
44
import { TransactionBuilder } from './transactionBuilder';
55
import { Transaction } from './transaction/transaction';
66
import utils from './utils';
@@ -50,7 +50,7 @@ export class TransferAcceptanceBuilder extends TransactionBuilder {
5050
* @throws Error if id is empty.
5151
*/
5252
commandId(id: string): this {
53-
if (!id.trim()) {
53+
if (!id || !id.trim()) {
5454
throw new Error('commandId must be a non-empty string');
5555
}
5656
this._commandId = id.trim();
@@ -66,7 +66,7 @@ export class TransferAcceptanceBuilder extends TransactionBuilder {
6666
* @throws Error if id is empty.
6767
*/
6868
contractId(id: string): this {
69-
if (!id.trim()) {
69+
if (!id || !id.trim()) {
7070
throw new Error('contractId must be a non-empty string');
7171
}
7272
this._contractId = id.trim();
@@ -81,23 +81,23 @@ export class TransferAcceptanceBuilder extends TransactionBuilder {
8181
* @throws Error if id is empty.
8282
*/
8383
actAs(id: string): this {
84-
if (!id.trim()) {
84+
if (!id || !id.trim()) {
8585
throw new Error('actAsPartyId must be a non-empty string');
8686
}
8787
this._actAsPartyId = id.trim();
8888
return this;
8989
}
9090

9191
/**
92-
* Builds and returns the CantonTransferAcceptRequest object from the builder's internal state.
92+
* Builds and returns the CantonTransferAcceptRejectRequest object from the builder's internal state.
9393
*
9494
* This method performs validation before constructing the object. If required fields are
9595
* missing or invalid, it throws an error.
9696
*
97-
* @returns {CantonTransferAcceptRequest} - A fully constructed and validated request object for transfer acceptance.
97+
* @returns {CantonTransferAcceptRejectRequest} - A fully constructed and validated request object for transfer acceptance.
9898
* @throws {Error} If any required field is missing or fails validation.
9999
*/
100-
toRequestObject(): CantonTransferAcceptRequest {
100+
toRequestObject(): CantonTransferAcceptRejectRequest {
101101
this.validate();
102102

103103
return {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 TransferRejectionBuilder extends TransactionBuilder {
9+
private _commandId: string;
10+
private _contractId: string;
11+
private _actAsPartyId: string;
12+
constructor(_coinConfig: Readonly<CoinConfig>) {
13+
super(_coinConfig);
14+
}
15+
16+
initBuilder(tx: Transaction): void {
17+
super.initBuilder(tx);
18+
this.setTransactionType();
19+
}
20+
21+
get transactionType(): TransactionType {
22+
return TransactionType.TransferReject;
23+
}
24+
25+
setTransactionType(): void {
26+
this.transaction.transactionType = TransactionType.TransferReject;
27+
}
28+
29+
setTransaction(transaction: CantonPrepareCommandResponse): void {
30+
this.transaction.prepareCommand = transaction;
31+
}
32+
33+
/** @inheritDoc */
34+
addSignature(publicKey: PublicKey, signature: Buffer): void {
35+
if (!this.transaction) {
36+
throw new InvalidTransactionError('transaction is empty!');
37+
}
38+
this._signatures.push({ publicKey, signature });
39+
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
40+
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
41+
this.transaction.signatures = signature.toString('base64');
42+
}
43+
44+
/**
45+
* Sets the unique id for the transfer rejection
46+
* Also sets the _id of the transaction
47+
*
48+
* @param id - A uuid
49+
* @returns The current builder instance for chaining.
50+
* @throws Error if id is empty.
51+
*/
52+
commandId(id: string): this {
53+
if (!id || !id.trim()) {
54+
throw new Error('commandId must be a non-empty string');
55+
}
56+
this._commandId = id.trim();
57+
// also set the transaction _id
58+
this.transaction.id = id.trim();
59+
return this;
60+
}
61+
62+
/**
63+
* Sets the rejection contract id the receiver needs to accept
64+
* @param id - canton rejection 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 receiver of the acceptance
78+
*
79+
* @param id - the receiver 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+
* This method performs validation before constructing the object. If required fields are
95+
* missing or invalid, it throws an error.
96+
*
97+
* @returns {CantonTransferAcceptRejectRequest} - A fully constructed and validated request object for transfer acceptance.
98+
* @throws {Error} If any required field is missing or fails validation.
99+
*/
100+
toRequestObject(): CantonTransferAcceptRejectRequest {
101+
this.validate();
102+
103+
return {
104+
commandId: this._commandId,
105+
contractId: this._contractId,
106+
verboseHashing: false,
107+
actAs: [this._actAsPartyId],
108+
readAs: [],
109+
};
110+
}
111+
112+
/**
113+
* Validates the internal state of the builder before building the request object.
114+
*
115+
* @private
116+
* @throws {Error} If any required field is missing or invalid.
117+
*/
118+
private validate(): void {
119+
if (!this._commandId) throw new Error('commandId is missing');
120+
if (!this._contractId) throw new Error('contractId is missing');
121+
if (!this._actAsPartyId) throw new Error('receiver partyId is missing');
122+
}
123+
}

modules/sdk-coin-canton/test/resources.ts

Lines changed: 15 additions & 0 deletions
Large diffs are not rendered by default.

modules/sdk-coin-canton/test/unit/builder/transferAccept/transferAcceptBuilder.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import should from 'should';
44
import { coins } from '@bitgo/statics';
55

66
import { TransferAcceptanceBuilder, Transaction } from '../../../../src';
7-
import { CantonTransferAcceptRequest } from '../../../../src/lib/iface';
7+
import { CantonTransferAcceptRejectRequest } from '../../../../src/lib/iface';
88

99
import { TransferAcceptance, TransferAcceptancePrepareResponse } from '../../../resources';
1010

@@ -15,7 +15,7 @@ describe('Transfer Acceptance Builder', () => {
1515
txBuilder.initBuilder(transferAcceptanceTx);
1616
const { commandId, contractId, partyId } = TransferAcceptance;
1717
txBuilder.commandId(commandId).contractId(contractId).actAs(partyId);
18-
const requestObj: CantonTransferAcceptRequest = txBuilder.toRequestObject();
18+
const requestObj: CantonTransferAcceptRejectRequest = txBuilder.toRequestObject();
1919
should.exist(requestObj);
2020
assert.equal(requestObj.commandId, commandId);
2121
assert.equal(requestObj.contractId, contractId);
@@ -54,18 +54,4 @@ describe('Transfer Acceptance Builder', () => {
5454
assert.equal(e.message, 'invalid raw transaction, hash not matching');
5555
}
5656
});
57-
58-
it('should throw error in validating raw transaction', function () {
59-
const txBuilder = new TransferAcceptanceBuilder(coins.get('tcanton'));
60-
const oneStepEnablementTx = new Transaction(coins.get('tcanton'));
61-
txBuilder.initBuilder(oneStepEnablementTx);
62-
const invalidPrepareResponse = TransferAcceptancePrepareResponse;
63-
invalidPrepareResponse.preparedTransactionHash = '+vlIXv6Vgd2ypPXD0mrdn7RlcSH4c2hCRj2/tXqqUVs=';
64-
oneStepEnablementTx.prepareCommand = invalidPrepareResponse;
65-
try {
66-
txBuilder.validateTransaction(oneStepEnablementTx);
67-
} catch (e) {
68-
assert.equal(e.message, 'invalid transaction');
69-
}
70-
});
7157
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import assert from 'assert';
2+
import should from 'should';
3+
4+
import { coins } from '@bitgo/statics';
5+
6+
import { Transaction, TransferRejectionBuilder } from '../../../../src';
7+
import { CantonTransferAcceptRejectRequest } from '../../../../src/lib/iface';
8+
9+
import { TransferRejection, TransferRejectionPrepareResponse } from '../../../resources';
10+
11+
describe('Transfer Rejection Builder', () => {
12+
it('should get the transfer rejection request object', function () {
13+
const txBuilder = new TransferRejectionBuilder(coins.get('tcanton'));
14+
const tx = new Transaction(coins.get('tcanton'));
15+
txBuilder.initBuilder(tx);
16+
const { commandId, contractId, partyId } = TransferRejection;
17+
txBuilder.commandId(commandId).contractId(contractId).actAs(partyId);
18+
const requestObj: CantonTransferAcceptRejectRequest = txBuilder.toRequestObject();
19+
should.exist(requestObj);
20+
assert.equal(requestObj.commandId, commandId);
21+
assert.equal(requestObj.contractId, contractId);
22+
assert.equal(requestObj.actAs.length, 1);
23+
const actAs = requestObj.actAs[0];
24+
assert.equal(actAs, partyId);
25+
});
26+
27+
it('should validate raw transaction', function () {
28+
const txBuilder = new TransferRejectionBuilder(coins.get('tcanton'));
29+
const tx = new Transaction(coins.get('tcanton'));
30+
txBuilder.initBuilder(tx);
31+
txBuilder.setTransaction(TransferRejectionPrepareResponse);
32+
txBuilder.validateRawTransaction(TransferRejectionPrepareResponse.preparedTransaction);
33+
});
34+
35+
it('should validate the transaction', function () {
36+
const txBuilder = new TransferRejectionBuilder(coins.get('tcanton'));
37+
const tx = new Transaction(coins.get('tcanton'));
38+
tx.prepareCommand = TransferRejectionPrepareResponse;
39+
txBuilder.initBuilder(tx);
40+
txBuilder.setTransaction(TransferRejectionPrepareResponse);
41+
txBuilder.validateTransaction(tx);
42+
});
43+
44+
it('should throw error in validating raw transaction', function () {
45+
const txBuilder = new TransferRejectionBuilder(coins.get('tcanton'));
46+
const tx = new Transaction(coins.get('tcanton'));
47+
txBuilder.initBuilder(tx);
48+
const invalidPrepareResponse = TransferRejectionPrepareResponse;
49+
invalidPrepareResponse.preparedTransactionHash = 'QFxX1WBdq7lZbSc45iKA3J/oOF9mrVLc3DeKphAjb15=';
50+
txBuilder.setTransaction(invalidPrepareResponse);
51+
try {
52+
txBuilder.validateRawTransaction(invalidPrepareResponse.preparedTransaction);
53+
} catch (e) {
54+
assert.equal(e.message, 'invalid raw transaction, hash not matching');
55+
}
56+
});
57+
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export enum TransactionType {
9393
TransferAccept,
9494
// canton transfer acknowledgement
9595
TransferAcknowledge,
96+
// canton transfer reject, 2-step
97+
TransferReject,
9698

9799
// trx
98100
FREEZE,

0 commit comments

Comments
 (0)