diff --git a/modules/sdk-coin-canton/src/lib/allocationRequestBuilder.ts b/modules/sdk-coin-canton/src/lib/allocationRequestBuilder.ts new file mode 100644 index 0000000000..82f107cf0a --- /dev/null +++ b/modules/sdk-coin-canton/src/lib/allocationRequestBuilder.ts @@ -0,0 +1,268 @@ +import { PublicKey, TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { AllocationRequest, CantonPrepareCommandResponse } from './iface'; +import { TransactionBuilder } from './transactionBuilder'; +import { Transaction } from './transaction/transaction'; + +/** + * Builder for an AllocationRequest txRequest — an internal, non-signable transaction + * that surfaces the DvP trade leg details to the allocating party. The party reviews + * this request and then submits a separate AllocationAllocate to lock their asset. + * + * setTransaction and addSignature are intentionally not implemented because this + * transaction type is never signed or broadcast directly. + */ +export class AllocationRequestBuilder extends TransactionBuilder { + private _updateId: string; + private _operatorId: string; + private _contractId: string; + private _tradeId: string; + private _transferLegId: string; + private _senderPartyId: string; + private _receiverPartyId: string; + private _amount: number; + private _token: string; + private _receiveToken: string; + private _receiveAmount: number; + private _allocateBefore: string; + private _settleBefore: string; + private _comment?: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + initBuilder(tx: Transaction): void { + super.initBuilder(tx); + this.setTransactionType(); + } + + get transactionType(): TransactionType { + return TransactionType.AllocationRequest; + } + + setTransactionType(): void { + this.transaction.transactionType = TransactionType.AllocationRequest; + } + + setTransaction(transaction: CantonPrepareCommandResponse): void { + throw new Error('Not implemented!'); + } + + /** @inheritDoc */ + addSignature(publicKey: PublicKey, signature: Buffer): void { + throw new Error('Not implemented!'); + } + + /** + * Sets the ledger update id of the AllocationRequest event. + * Also sets the transaction id. + * @param id - ledger update id (txHash) + */ + updateId(id: string): this { + if (!id || !id.trim()) { + throw new Error('updateId must be a non-empty string'); + } + this._updateId = id.trim(); + this.transaction.id = id.trim(); + return this; + } + + /** + * Sets the operator party id (settlement executor). + * @param id - operator party id + */ + 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 settlement batch contract id. + * @param id - settlement batch contract id + */ + 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 trade identifier. + * @param id - trade id + */ + tradeId(id: string): this { + if (!id || !id.trim()) { + throw new Error('tradeId must be a non-empty string'); + } + this._tradeId = id.trim(); + return this; + } + + /** + * Sets the specific leg id being allocated (e.g. `${tradeId}-security-leg`). + * @param id - transfer leg id + */ + transferLegId(id: string): this { + if (!id || !id.trim()) { + throw new Error('transferLegId must be a non-empty string'); + } + this._transferLegId = id.trim(); + return this; + } + + /** + * Sets the party performing this allocation (sender of this leg). + * @param id - sender party id + */ + senderPartyId(id: string): this { + if (!id || !id.trim()) { + throw new Error('senderPartyId must be a non-empty string'); + } + this._senderPartyId = id.trim(); + return this; + } + + /** + * Sets the counterparty receiving the allocated asset. + * @param id - receiver party id + */ + receiverPartyId(id: string): this { + if (!id || !id.trim()) { + throw new Error('receiverPartyId must be a non-empty string'); + } + this._receiverPartyId = id.trim(); + return this; + } + + /** + * Sets the quantity to allocate. + * @param amount - allocation amount + */ + amount(amount: number): this { + if (isNaN(amount) || amount <= 0) { + throw new Error('amount must be a positive number'); + } + this._amount = amount; + return this; + } + + /** + * Sets the BitGo token identifier for the asset being allocated. + * @param token - token identifier + */ + token(token: string): this { + if (!token || !token.trim()) { + throw new Error('token must be a non-empty string'); + } + this._token = token.trim(); + return this; + } + + /** + * Sets the BitGo token identifier the allocating party will receive on settlement. + * @param token - receive token identifier + */ + receiveToken(token: string): this { + if (!token || !token.trim()) { + throw new Error('receiveToken must be a non-empty string'); + } + this._receiveToken = token.trim(); + return this; + } + + /** + * Sets the quantity the allocating party will receive on settlement. + * @param amount - receive amount + */ + receiveAmount(amount: number): this { + if (isNaN(amount) || amount <= 0) { + throw new Error('receiveAmount must be a positive number'); + } + this._receiveAmount = amount; + return this; + } + + /** + * Sets the ISO 8601 deadline by which allocation must be submitted. + * @param deadline - allocate-before timestamp + */ + allocateBefore(deadline: string): this { + if (!deadline || !deadline.trim()) { + throw new Error('allocateBefore must be a non-empty string'); + } + this._allocateBefore = deadline.trim(); + return this; + } + + /** + * Sets the ISO 8601 deadline by which settlement must complete. + * @param deadline - settle-before timestamp + */ + settleBefore(deadline: string): this { + if (!deadline || !deadline.trim()) { + throw new Error('settleBefore must be a non-empty string'); + } + this._settleBefore = deadline.trim(); + return this; + } + + /** + * Sets an optional free-form comment. + * @param comment - comment string + */ + comment(comment: string): this { + this._comment = comment; + return this; + } + + /** + * Builds and returns the AllocationRequest object from the builder's internal state. + * + * @returns {AllocationRequest} - A fully constructed and validated request object. + * @throws {Error} If any required field is missing or fails validation. + */ + toRequestObject(): AllocationRequest { + this.validate(); + const result: AllocationRequest = { + updateId: this._updateId, + operatorId: this._operatorId, + contractId: this._contractId, + tradeId: this._tradeId, + transferLegId: this._transferLegId, + senderPartyId: this._senderPartyId, + receiverPartyId: this._receiverPartyId, + amount: this._amount, + token: this._token, + receiveToken: this._receiveToken, + receiveAmount: this._receiveAmount, + allocateBefore: this._allocateBefore, + settleBefore: this._settleBefore, + }; + if (this._comment !== undefined) { + result.comment = this._comment; + } + return result; + } + + private validate(): void { + if (!this._updateId) throw new Error('updateId is missing'); + if (!this._operatorId) throw new Error('operatorId is missing'); + if (!this._contractId) throw new Error('contractId is missing'); + if (!this._tradeId) throw new Error('tradeId is missing'); + if (!this._transferLegId) throw new Error('transferLegId is missing'); + if (!this._senderPartyId) throw new Error('senderPartyId is missing'); + if (!this._receiverPartyId) throw new Error('receiverPartyId is missing'); + if (this._amount === undefined || this._amount === null) throw new Error('amount is missing'); + if (!this._token) throw new Error('token is missing'); + if (!this._receiveToken) throw new Error('receiveToken is missing'); + if (this._receiveAmount === undefined || this._receiveAmount === null) throw new Error('receiveAmount is missing'); + if (!this._allocateBefore) throw new Error('allocateBefore is missing'); + if (!this._settleBefore) throw new Error('settleBefore is missing'); + } +} diff --git a/modules/sdk-coin-canton/src/lib/iface.ts b/modules/sdk-coin-canton/src/lib/iface.ts index 68275dd71b..a943c29e4e 100644 --- a/modules/sdk-coin-canton/src/lib/iface.ts +++ b/modules/sdk-coin-canton/src/lib/iface.ts @@ -22,6 +22,7 @@ export interface TxData { amount: string; acknowledgeData?: TransferAcknowledge; cosignDelegationProposalData?: CosignDelegationProposal; + allocationRequestData?: AllocationRequest; memoId?: string; token?: string; } @@ -114,6 +115,7 @@ export interface PartySignature { export interface TransactionBroadcastData { acknowledgeData?: TransferAcknowledge; cosignDelegationProposalData?: CosignDelegationProposal; + allocationRequestData?: AllocationRequest; prepareCommandResponse?: CantonPrepareCommandResponse; txType: string; preparedTransaction?: string; @@ -185,3 +187,25 @@ export interface CantonAllocationAllocateRequest { senderPartyId: string; comment?: string; } + +/** + * Internal (non-signable) data for an AllocationRequest txRequest. + * Surfaces the full DvP trade leg to the allocating party so they can + * review and then submit an AllocationAllocate. + */ +export interface AllocationRequest { + updateId: string; + operatorId: string; + contractId: string; + tradeId: string; + transferLegId: string; + senderPartyId: string; + receiverPartyId: string; + amount: number; + token: string; + receiveToken: string; + receiveAmount: number; + allocateBefore: string; + settleBefore: string; + comment?: string; +} diff --git a/modules/sdk-coin-canton/src/lib/index.ts b/modules/sdk-coin-canton/src/lib/index.ts index 4c4954f5e4..39c3cb4dc3 100644 --- a/modules/sdk-coin-canton/src/lib/index.ts +++ b/modules/sdk-coin-canton/src/lib/index.ts @@ -2,6 +2,7 @@ import * as Utils from './utils'; import * as Interface from './iface'; export { AllocationAllocateBuilder } from './allocationAllocateBuilder'; +export { AllocationRequestBuilder } from './allocationRequestBuilder'; export { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder'; export { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder'; export { KeyPair } from './keyPair'; diff --git a/modules/sdk-coin-canton/src/lib/transaction/transaction.ts b/modules/sdk-coin-canton/src/lib/transaction/transaction.ts index d24d7e0122..33988e3d8d 100644 --- a/modules/sdk-coin-canton/src/lib/transaction/transaction.ts +++ b/modules/sdk-coin-canton/src/lib/transaction/transaction.ts @@ -8,6 +8,7 @@ import { } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { + AllocationRequest, CantonPrepareCommandResponse, CosignDelegationProposal, MultiHashSignature, @@ -26,6 +27,7 @@ export class Transaction extends BaseTransaction { private _signerFingerprint: string; private _acknowledgeData: TransferAcknowledge; private _cosignDelegationProposalData: CosignDelegationProposal; + private _allocationRequestData: AllocationRequest; constructor(coinConfig: Readonly) { super(coinConfig); @@ -51,6 +53,10 @@ export class Transaction extends BaseTransaction { this._cosignDelegationProposalData = data; } + set allocationRequestData(data: AllocationRequest) { + this._allocationRequestData = data; + } + get id(): string { if (!this._id) { throw new InvalidTransactionError('transaction is is not set'); @@ -100,6 +106,17 @@ export class Transaction extends BaseTransaction { }; return Buffer.from(JSON.stringify(minData)).toString('base64'); } + if (this._type === TransactionType.AllocationRequest) { + if (!this._allocationRequestData) { + throw new InvalidTransactionError('AllocationRequestData is not set'); + } + const minData: TransactionBroadcastData = { + txType: TransactionType[this._type], + submissionId: this.id, + allocationRequestData: this._allocationRequestData, + }; + return Buffer.from(JSON.stringify(minData)).toString('base64'); + } if (!this._prepareCommand) { throw new InvalidTransactionError('Empty transaction data'); } @@ -168,6 +185,13 @@ export class Transaction extends BaseTransaction { result.cosignDelegationProposalData = this._cosignDelegationProposalData; return result; } + if (this._type === TransactionType.AllocationRequest) { + if (!this._allocationRequestData) { + throw new InvalidTransactionError('AllocationRequestData is not set'); + } + result.allocationRequestData = this._allocationRequestData; + return result; + } if (!this._prepareCommand || !this._prepareCommand.preparedTransaction) { throw new InvalidTransactionError('Empty transaction data'); } @@ -191,7 +215,11 @@ export class Transaction extends BaseTransaction { } get signablePayload(): Buffer { - if (this._type === TransactionType.TransferAcknowledge || this._type === TransactionType.CosignDelegationProposal) { + if ( + this._type === TransactionType.TransferAcknowledge || + this._type === TransactionType.CosignDelegationProposal || + this._type === TransactionType.AllocationRequest + ) { return Buffer.from(DUMMY_HASH, 'base64'); } if (!this._prepareCommand) { @@ -213,6 +241,10 @@ export class Transaction extends BaseTransaction { if (decoded.cosignDelegationProposalData) { this.cosignDelegationProposalData = decoded.cosignDelegationProposalData; } + } else if (this.type === TransactionType.AllocationRequest) { + if (decoded.allocationRequestData) { + this.allocationRequestData = decoded.allocationRequestData; + } } else { if (decoded.prepareCommandResponse) { this.prepareCommand = decoded.prepareCommandResponse; diff --git a/modules/sdk-coin-canton/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-canton/src/lib/transactionBuilderFactory.ts index 143b7f3d66..5f98c788ca 100644 --- a/modules/sdk-coin-canton/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-canton/src/lib/transactionBuilderFactory.ts @@ -6,6 +6,7 @@ import { } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { AllocationAllocateBuilder } from './allocationAllocateBuilder'; +import { AllocationRequestBuilder } from './allocationRequestBuilder'; import { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder'; import { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder'; import { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder'; @@ -60,6 +61,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { case TransactionType.AllocationAllocate: { return this.getAllocationAllocateBuilder(tx); } + case TransactionType.AllocationRequest: { + return this.getAllocationRequestBuilder(tx); + } default: { throw new InvalidTransactionError('unsupported transaction'); } @@ -71,6 +75,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return TransactionBuilderFactory.initializeBuilder(tx, new AllocationAllocateBuilder(this._coinConfig)); } + getAllocationRequestBuilder(tx?: Transaction): AllocationRequestBuilder { + return TransactionBuilderFactory.initializeBuilder(tx, new AllocationRequestBuilder(this._coinConfig)); + } + getOneStepPreapprovalBuilder(tx?: Transaction): OneStepPreApprovalBuilder { return TransactionBuilderFactory.initializeBuilder(tx, new OneStepPreApprovalBuilder(this._coinConfig)); } diff --git a/modules/sdk-coin-canton/test/unit/builder/allocationRequest/allocationRequestBuilder.ts b/modules/sdk-coin-canton/test/unit/builder/allocationRequest/allocationRequestBuilder.ts new file mode 100644 index 0000000000..69044a5f09 --- /dev/null +++ b/modules/sdk-coin-canton/test/unit/builder/allocationRequest/allocationRequestBuilder.ts @@ -0,0 +1,422 @@ +import assert from 'assert'; +import should from 'should'; + +import { coins } from '@bitgo/statics'; + +import { AllocationRequestBuilder, Transaction } from '../../../../src'; +import { AllocationRequest } from '../../../../src/lib/iface'; + +const updateId = '12205b0b024b6f72c5484696271e8e87d7c1e0cc7bed5e9c901088b41ebe97a09a43'; +const operatorId = 'treasury-tokenization-1::1220906cb6b369890324880c977a44d10e91b70b4cc9abe62d031bdafcf5dabecf89'; +const contractId = + '0020108b7aee0e0215538668a7ebb00e7e811135ff2ebf23137760056e077b9347ca121220dfa25cd905caba01345b515d1002af94b0ba2a9c2e9a6751c07ed28795454a9e'; +const tradeId = 'CASH-TRADE-662AA1BA'; +const transferLegId = 'CASH-TRADE-662AA1BA-security-leg'; +const senderPartyId = 'ravi-2-step-party-new::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2'; +const receiverPartyId = 'ravi-2-step-party::122092e7d33ac10c0f3d55976342f37555df05da5b742956d56a62ae2367769079d2'; +const amount = 70; +const token = 'tcanton:testtoken'; +const receiveToken = 'tcanton:testcoin1'; +const receiveAmount = 80; +const allocateBefore = '2026-05-06T16:46:17.184609Z'; +const settleBefore = '2026-05-07T16:46:17.184609Z'; +const comment = 'security leg allocation'; + +// Helper to set all required fields on a builder +function buildWithAllRequired(txBuilder: AllocationRequestBuilder): AllocationRequestBuilder { + return txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); +} + +describe('AllocationRequest Builder', () => { + it('should build the allocation request object with all fields', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + buildWithAllRequired(txBuilder).comment(comment); + const requestObj: AllocationRequest = txBuilder.toRequestObject(); + should.exist(requestObj); + assert.equal(requestObj.updateId, updateId); + assert.equal(requestObj.operatorId, operatorId); + assert.equal(requestObj.contractId, contractId); + assert.equal(requestObj.tradeId, tradeId); + assert.equal(requestObj.transferLegId, transferLegId); + assert.equal(requestObj.senderPartyId, senderPartyId); + assert.equal(requestObj.receiverPartyId, receiverPartyId); + assert.equal(requestObj.amount, amount); + assert.equal(requestObj.token, token); + assert.equal(requestObj.receiveToken, receiveToken); + assert.equal(requestObj.receiveAmount, receiveAmount); + assert.equal(requestObj.allocateBefore, allocateBefore); + assert.equal(requestObj.settleBefore, settleBefore); + assert.equal(requestObj.comment, comment); + }); + + it('should build the allocation request object without optional comment', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + buildWithAllRequired(txBuilder); + const requestObj: AllocationRequest = txBuilder.toRequestObject(); + should.exist(requestObj); + assert.equal(requestObj.comment, undefined); + }); + + it('should set the transaction id to the updateId', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + const tx = new Transaction(coins.get('tcanton')); + txBuilder.initBuilder(tx); + txBuilder.updateId(updateId); + assert.equal(tx.id, updateId); + }); + + // --- missing required field tests --- + + it('should throw if updateId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /updateId is missing/); + }); + + it('should throw if operatorId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /operatorId is missing/); + }); + + it('should throw if contractId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /contractId is missing/); + }); + + it('should throw if tradeId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /tradeId is missing/); + }); + + it('should throw if transferLegId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /transferLegId is missing/); + }); + + it('should throw if senderPartyId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /senderPartyId is missing/); + }); + + it('should throw if receiverPartyId is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /receiverPartyId is missing/); + }); + + it('should throw if token is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /token is missing/); + }); + + it('should throw if receiveToken is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /receiveToken is missing/); + }); + + it('should throw if amount was never set', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /amount is missing/); + }); + + it('should throw if receiveAmount was never set', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .allocateBefore(allocateBefore) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /receiveAmount is missing/); + }); + + it('should throw if allocateBefore is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .settleBefore(settleBefore); + assert.throws(() => txBuilder.toRequestObject(), /allocateBefore is missing/); + }); + + it('should throw if settleBefore is missing', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + txBuilder.initBuilder(new Transaction(coins.get('tcanton'))); + txBuilder + .updateId(updateId) + .operatorId(operatorId) + .contractId(contractId) + .tradeId(tradeId) + .transferLegId(transferLegId) + .senderPartyId(senderPartyId) + .receiverPartyId(receiverPartyId) + .amount(amount) + .token(token) + .receiveToken(receiveToken) + .receiveAmount(receiveAmount) + .allocateBefore(allocateBefore); + assert.throws(() => txBuilder.toRequestObject(), /settleBefore is missing/); + }); + + // --- invalid setter argument tests --- + + it('should throw if updateId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.updateId(''), /updateId must be a non-empty string/); + }); + + it('should throw if operatorId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.operatorId(''), /operatorId must be a non-empty string/); + }); + + it('should throw if contractId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.contractId(''), /contractId must be a non-empty string/); + }); + + it('should throw if tradeId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.tradeId(''), /tradeId must be a non-empty string/); + }); + + it('should throw if transferLegId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.transferLegId(''), /transferLegId must be a non-empty string/); + }); + + it('should throw if senderPartyId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.senderPartyId(''), /senderPartyId must be a non-empty string/); + }); + + it('should throw if receiverPartyId is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.receiverPartyId(''), /receiverPartyId must be a non-empty string/); + }); + + it('should throw if amount is zero', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.amount(0), /amount must be a positive number/); + }); + + it('should throw if amount is negative', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.amount(-5), /amount must be a positive number/); + }); + + it('should throw if token is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.token(''), /token must be a non-empty string/); + }); + + it('should throw if receiveToken is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.receiveToken(''), /receiveToken must be a non-empty string/); + }); + + it('should throw if receiveAmount is zero', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.receiveAmount(0), /receiveAmount must be a positive number/); + }); + + it('should throw if receiveAmount is negative', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.receiveAmount(-1), /receiveAmount must be a positive number/); + }); + + it('should throw if allocateBefore is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.allocateBefore(''), /allocateBefore must be a non-empty string/); + }); + + it('should throw if settleBefore is an empty string', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.settleBefore(''), /settleBefore must be a non-empty string/); + }); + + // --- not-implemented methods --- + + it('should throw on setTransaction', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.setTransaction({} as any), /Not implemented/); + }); + + it('should throw on addSignature', function () { + const txBuilder = new AllocationRequestBuilder(coins.get('tcanton')); + assert.throws(() => txBuilder.addSignature({} as any, Buffer.from('')), /Not implemented/); + }); +}); diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index 0d72493909..09ed9c8fc7 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -103,6 +103,8 @@ export enum TransactionType { CosignDelegationAccept, // canton allocation allocate AllocationAllocate, + // canton allocation request (internal/dummy txRequest surfacing DvP trade details to the allocating party) + AllocationRequest, // trx FREEZE,