Skip to content

Commit 04a0ae0

Browse files
committed
feat(sdk-coin-canton): added transfer acceptance builder
Ticket: COIN-6018
1 parent f8295bb commit 04a0ae0

File tree

8 files changed

+231
-12
lines changed

8 files changed

+231
-12
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,8 @@ export interface WalletInitRequest {
5757
observingParticipantUids: string[];
5858
}
5959

60-
export interface OneStepEnablementRequest {
60+
export interface CantonPrepareCommandRequest {
6161
commandId: string;
62-
receiverId: string;
6362
verboseHashing: boolean;
6463
actAs: string[];
6564
readAs: string[];
@@ -81,3 +80,11 @@ export interface WalletInitBroadcastData {
8180
onboardingTransactions: OnboardingTransaction[];
8281
multiHashSignatures: MultiHashSignature[];
8382
}
83+
84+
export interface CantonOneStepEnablementRequest extends CantonPrepareCommandRequest {
85+
receiverId: string;
86+
}
87+
88+
export interface CantonTransferAcceptRequest extends CantonPrepareCommandRequest {
89+
contractId: string;
90+
}

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

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

44
export { KeyPair } from './keyPair';
55
export { Transaction } from './transaction/transaction';
6+
export { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
67
export { TransactionBuilder } from './transactionBuilder';
78
export { TransactionBuilderFactory } from './transactionBuilderFactory';
89
export { WalletInitBuilder } from './walletInitBuilder';

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TransactionType } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3-
import { CantonPrepareCommandResponse, OneStepEnablementRequest } from './iface';
3+
import { CantonPrepareCommandResponse, CantonOneStepEnablementRequest } from './iface';
44
import { TransactionBuilder } from './transactionBuilder';
55
import { Transaction } from './transaction/transaction';
66

@@ -62,15 +62,15 @@ export class OneStepPreApprovalBuilder extends TransactionBuilder {
6262
}
6363

6464
/**
65-
* Builds and returns the OneStepEnablementRequest object from the builder's internal state.
65+
* Builds and returns the CantonOneStepEnablementRequest object from the builder's internal state.
6666
*
6767
* This method performs validation before constructing the object. If required fields are
6868
* missing or invalid, it throws an error.
6969
*
70-
* @returns {OneStepEnablementRequest} - A fully constructed and validated request object for 1-step enablement.
70+
* @returns {CantonOneStepEnablementRequest} - A fully constructed and validated request object for 1-step enablement.
7171
* @throws {Error} If any required field is missing or fails validation.
7272
*/
73-
toRequestObject(): OneStepEnablementRequest {
73+
toRequestObject(): CantonOneStepEnablementRequest {
7474
this.validate();
7575

7676
return {

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

Lines changed: 16 additions & 3 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 { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
89
import { TransactionBuilder } from './transactionBuilder';
910
import { TransferBuilder } from './transferBuilder';
1011
import { Transaction } from './transaction/transaction';
@@ -24,13 +25,25 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
2425
} catch {
2526
const tx = new Transaction(this._coinConfig);
2627
tx.fromRawTransaction(raw);
27-
if (tx.type === TransactionType.Send) {
28-
return this.getTransferBuilder(tx);
28+
switch (tx.type) {
29+
case TransactionType.Send: {
30+
return this.getTransferBuilder(tx);
31+
}
32+
case TransactionType.TransferAccept: {
33+
return this.getTransferAcceptanceBuilder(tx);
34+
}
35+
default: {
36+
throw new InvalidTransactionError('unsupported transaction');
37+
}
2938
}
30-
throw new InvalidTransactionError('unsupported transaction');
3139
}
3240
}
3341

42+
/** @inheritdoc */
43+
getTransferAcceptanceBuilder(tx?: Transaction): TransferAcceptanceBuilder {
44+
return TransactionBuilderFactory.initializeBuilder(tx, new TransferAcceptanceBuilder(this._coinConfig));
45+
}
46+
3447
/** @inheritdoc */
3548
getTransferBuilder(tx?: Transaction): TransferBuilder {
3649
return TransactionBuilderFactory.initializeBuilder(tx, new TransferBuilder(this._coinConfig));
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { CantonPrepareCommandResponse, CantonTransferAcceptRequest } from './iface';
4+
import { TransactionBuilder } from './transactionBuilder';
5+
import { Transaction } from './transaction/transaction';
6+
7+
export class TransferAcceptanceBuilder extends TransactionBuilder {
8+
private _commandId: string;
9+
private _contractId: string;
10+
private _actAsPartyId: string;
11+
constructor(_coinConfig: Readonly<CoinConfig>) {
12+
super(_coinConfig);
13+
}
14+
15+
initBuilder(tx: Transaction): void {
16+
super.initBuilder(tx);
17+
this.setTransactionType();
18+
}
19+
20+
get transactionType(): TransactionType {
21+
return TransactionType.TransferAccept;
22+
}
23+
24+
setTransactionType(): void {
25+
this.transaction.transactionType = TransactionType.TransferAccept;
26+
}
27+
28+
setTransaction(transaction: CantonPrepareCommandResponse): void {
29+
this.transaction.prepareCommand = transaction;
30+
}
31+
32+
/**
33+
* Sets the unique id for the transfer acceptance
34+
* Also sets the _id of the transaction
35+
*
36+
* @param id - A uuid
37+
* @returns The current builder instance for chaining.
38+
* @throws Error if id is empty.
39+
*/
40+
commandId(id: string): this {
41+
if (!id.trim()) {
42+
throw new Error('commandId must be a non-empty string');
43+
}
44+
this._commandId = id.trim();
45+
// also set the transaction _id
46+
this.transaction.id = id.trim();
47+
return this;
48+
}
49+
50+
/**
51+
* Sets the acceptance contract id the receiver needs to accept
52+
* @param id - canton acceptance contract id
53+
* @returns The current builder instance for chaining.
54+
* @throws Error if id is empty.
55+
*/
56+
contractId(id: string): this {
57+
if (!id.trim()) {
58+
throw new Error('contractId must be a non-empty string');
59+
}
60+
this._contractId = id.trim();
61+
return this;
62+
}
63+
64+
/**
65+
* Sets the receiver of the acceptance
66+
*
67+
* @param id - the receiver party id (address)
68+
* @returns The current builder instance for chaining.
69+
* @throws Error if id is empty.
70+
*/
71+
actAs(id: string): this {
72+
if (!id.trim()) {
73+
throw new Error('actAsPartyId must be a non-empty string');
74+
}
75+
this._actAsPartyId = id.trim();
76+
return this;
77+
}
78+
79+
/**
80+
* Builds and returns the CantonTransferAcceptRequest object from the builder's internal state.
81+
*
82+
* This method performs validation before constructing the object. If required fields are
83+
* missing or invalid, it throws an error.
84+
*
85+
* @returns {CantonTransferAcceptRequest} - A fully constructed and validated request object for transfer acceptance.
86+
* @throws {Error} If any required field is missing or fails validation.
87+
*/
88+
toRequestObject(): CantonTransferAcceptRequest {
89+
this.validate();
90+
91+
return {
92+
commandId: this._commandId,
93+
contractId: this._contractId,
94+
verboseHashing: false,
95+
actAs: [this._actAsPartyId],
96+
readAs: [],
97+
};
98+
}
99+
100+
/**
101+
* Validates the internal state of the builder before building the request object.
102+
*
103+
* @private
104+
* @throws {Error} If any required field is missing or invalid.
105+
*/
106+
private validate(): void {
107+
if (!this._commandId) throw new Error('commandId is missing');
108+
if (!this._contractId) throw new Error('contractId is missing');
109+
if (!this._actAsPartyId) throw new Error('receiver partyId is missing');
110+
}
111+
}

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/oneStepEnablement/oneStepEnablementBuilder.ts

Lines changed: 3 additions & 3 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 { Transaction } from '../../../../src';
7-
import { OneStepEnablementRequest } from '../../../../src/lib/iface';
7+
import { CantonOneStepEnablementRequest } from '../../../../src/lib/iface';
88
import { OneStepPreApprovalBuilder } from '../../../../src/lib/oneStepPreApprovalBuilder';
99

1010
import {
@@ -14,13 +14,13 @@ import {
1414
} from '../../../resources';
1515

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

0 commit comments

Comments
 (0)