Skip to content

Commit fcaa7e4

Browse files
authored
Merge pull request #7321 from BitGo/COIN-6018
feat(sdk-coin-canton): added transfer acceptance builder
2 parents 860cc73 + d1f3794 commit fcaa7e4

File tree

9 files changed

+233
-12
lines changed

9 files changed

+233
-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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import * as Utils from './utils';
22
import * as Interface from './iface';
33

44
export { KeyPair } from './keyPair';
5+
export { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
56
export { Transaction } from './transaction/transaction';
7+
export { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
68
export { TransactionBuilder } from './transactionBuilder';
79
export { TransactionBuilderFactory } from './transactionBuilderFactory';
810
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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import assert from 'assert';
2+
import should from 'should';
3+
4+
import { coins } from '@bitgo/statics';
5+
6+
import { TransferAcceptanceBuilder, Transaction } from '../../../../src';
7+
import { CantonTransferAcceptRequest } from '../../../../src/lib/iface';
8+
9+
import { TransferAcceptance, TransferAcceptancePrepareResponse } from '../../../resources';
10+
11+
describe('Transfer Acceptance Builder', () => {
12+
it('should get the transfer acceptance request object', function () {
13+
const txBuilder = new TransferAcceptanceBuilder(coins.get('tcanton'));
14+
const transferAcceptanceTx = new Transaction(coins.get('tcanton'));
15+
txBuilder.initBuilder(transferAcceptanceTx);
16+
const { commandId, contractId, partyId } = TransferAcceptance;
17+
txBuilder.commandId(commandId).contractId(contractId).actAs(partyId);
18+
const requestObj: CantonTransferAcceptRequest = 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 TransferAcceptanceBuilder(coins.get('tcanton'));
29+
const transferAcceptanceTx = new Transaction(coins.get('tcanton'));
30+
txBuilder.initBuilder(transferAcceptanceTx);
31+
txBuilder.setTransaction(TransferAcceptancePrepareResponse);
32+
txBuilder.validateRawTransaction(TransferAcceptancePrepareResponse.preparedTransaction);
33+
});
34+
35+
it('should validate the transaction', function () {
36+
const txBuilder = new TransferAcceptanceBuilder(coins.get('tcanton'));
37+
const transferAcceptanceTx = new Transaction(coins.get('tcanton'));
38+
transferAcceptanceTx.prepareCommand = TransferAcceptancePrepareResponse;
39+
txBuilder.initBuilder(transferAcceptanceTx);
40+
txBuilder.setTransaction(TransferAcceptancePrepareResponse);
41+
txBuilder.validateTransaction(transferAcceptanceTx);
42+
});
43+
44+
it('should throw error in validating raw transaction', function () {
45+
const txBuilder = new TransferAcceptanceBuilder(coins.get('tcanton'));
46+
const transferAcceptanceTx = new Transaction(coins.get('tcanton'));
47+
txBuilder.initBuilder(transferAcceptanceTx);
48+
const invalidPrepareResponse = TransferAcceptancePrepareResponse;
49+
invalidPrepareResponse.preparedTransactionHash = '+vlIXv6Vgd2ypPXD0mrdn7RlcSH4c2hCRj2/tXqqUVs=';
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+
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+
});
71+
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export enum TransactionType {
8989
FlushERC1155,
9090
// Set up 1-step pre-approval for canton
9191
OneStepPreApproval,
92+
// canton transfer accept, 2-step
93+
TransferAccept,
9294

9395
// trx
9496
FREEZE,

0 commit comments

Comments
 (0)