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
1 change: 1 addition & 0 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class Canton extends BaseCoin {
case TransactionType.CosignDelegationProposal:
case TransactionType.AllocationAllocate:
case TransactionType.AllocationRequest:
case TransactionType.EndInvestorOnboardingOffer:
// There is no recipient info to verify for these transaction types, so always return true.
return true;
case TransactionType.CantonCommand:
Expand Down
125 changes: 125 additions & 0 deletions modules/sdk-coin-canton/src/lib/endInvestorOnboardingOfferBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { PublicKey, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CantonPrepareCommandResponse, EndInvestorOnboardingOfferData } from './iface';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction/transaction';

/**
* Builder for an EndInvestorOnboardingOffer txRequest — an internal, non-signable transaction
* that records an on-chain EndInvestorOnboardingOffer contract on the end investor's wallet
* so they can accept or reject. Built locally without an IMS call because the inviting
* participant may be external (non-BitGo) and therefore not registered in BitGo's IMS.
*
* setTransaction and addSignature are intentionally not implemented because this
* transaction type is never signed or broadcast directly.
*/
export class EndInvestorOnboardingOfferBuilder extends TransactionBuilder {
private _contractId: string;
private _endInvestorPartyId: string;
private _participantPartyId: string;
private _comment?: string;

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

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

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

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

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

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

/**
* Sets the on-chain contractId of the EndInvestorOnboardingOffer.
* Also sets the transaction id.
* @param id - contract id (from pendingContractId)
*/
contractId(id: string): this {
if (!id || !id.trim()) {
throw new Error('contractId must be a non-empty string');
}
this._contractId = id.trim();
this.transaction.id = id.trim();
return this;
}

/**
* Sets the Canton party ID of the end investor being onboarded.
* @param id - end investor party id
*/
endInvestorPartyId(id: string): this {
if (!id || !id.trim()) {
throw new Error('endInvestorPartyId must be a non-empty string');
}
this._endInvestorPartyId = id.trim();
return this;
}

/**
* Sets the Canton party ID of the participant that created the offer.
* @param id - participant party id (may be external to BitGo)
*/
participantPartyId(id: string): this {
if (!id || !id.trim()) {
throw new Error('participantPartyId must be a non-empty string');
}
this._participantPartyId = id.trim();
return this;
}

/**
* Sets an optional free-form comment from the participant.
* @param comment - comment string
*/
comment(comment: string): this {
this._comment = comment;
return this;
}

/**
* Builds and returns the EndInvestorOnboardingOfferData object from the builder's internal state.
*
* @returns {EndInvestorOnboardingOfferData} - A fully constructed and validated data object.
* @throws {Error} If any required field is missing.
*/
toRequestObject(): EndInvestorOnboardingOfferData {
this.validate();
const result: EndInvestorOnboardingOfferData = {
contractId: this._contractId,
endInvestorPartyId: this._endInvestorPartyId,
participantPartyId: this._participantPartyId,
};
if (this._comment !== undefined) {
result.comment = this._comment;
}
return result;
}

protected async buildImplementation(): Promise<Transaction> {
this.validate();
this.transaction.endInvestorOnboardingOfferData = this.toRequestObject();
return this.transaction;
}

private validate(): void {
if (!this._contractId) throw new Error('contractId is missing');
if (!this._endInvestorPartyId) throw new Error('endInvestorPartyId is missing');
if (!this._participantPartyId) throw new Error('participantPartyId is missing');
}
}
19 changes: 19 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface TxData {
acknowledgeData?: TransferAcknowledge;
cosignDelegationProposalData?: CosignDelegationProposal;
allocationRequestData?: AllocationRequest;
endInvestorOnboardingOfferData?: EndInvestorOnboardingOfferData;
memoId?: string;
token?: string;
cantonCommand?: CantonCommandExplain;
Expand Down Expand Up @@ -128,6 +129,7 @@ export interface TransactionBroadcastData {
acknowledgeData?: TransferAcknowledge;
cosignDelegationProposalData?: CosignDelegationProposal;
allocationRequestData?: AllocationRequest;
endInvestorOnboardingOfferData?: EndInvestorOnboardingOfferData;
prepareCommandResponse?: CantonPrepareCommandResponse;
txType: string;
preparedTransaction?: string;
Expand Down Expand Up @@ -226,6 +228,23 @@ export interface AllocationRequest {
comment?: string;
}

/**
* Internal (non-signable) data for an EndInvestorOnboardingOffer txRequest.
* Records an on-chain EndInvestorOnboardingOffer contract on the end investor's wallet
* so they can accept or reject. Built locally without an IMS call — the participant
* party may be external (non-BitGo) and therefore not registered in BitGo's IMS.
*/
export interface EndInvestorOnboardingOfferData {
/** The on-chain contractId of the EndInvestorOnboardingOffer (from pendingContractId). */
contractId: string;
/** Canton party ID of the end investor being onboarded. */
endInvestorPartyId: string;
/** Canton party ID of the participant that created the offer (may be external to BitGo). */
participantPartyId: string;
/** Optional comment from the participant. */
comment?: string;
}

export const CANTON_COMMAND_KEYS = ['CreateCommand', 'ExerciseCommand'] as const;
export type CantonCommandKind = (typeof CANTON_COMMAND_KEYS)[number];

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
Expand Up @@ -6,6 +6,7 @@ export { AllocationAllocateBuilder } from './allocationAllocateBuilder';
export { AllocationAllocateWithdrawnBuilder } from './allocationAllocateWithdrawnBuilder';
export { AllocationRejectBuilder } from './allocationRejectBuilder';
export { AllocationRequestBuilder } from './allocationRequestBuilder';
export { EndInvestorOnboardingOfferBuilder } from './endInvestorOnboardingOfferBuilder';
export { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder';
export { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
export { KeyPair } from './keyPair';
Expand Down
46 changes: 45 additions & 1 deletion modules/sdk-coin-canton/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CantonCommandExplain,
CantonPrepareCommandResponse,
CosignDelegationProposal,
EndInvestorOnboardingOfferData,
MultiHashSignature,
PartySignature,
PreparedTxnParsedInfo,
Expand All @@ -29,6 +30,7 @@ export class Transaction extends BaseTransaction {
private _acknowledgeData: TransferAcknowledge;
private _cosignDelegationProposalData: CosignDelegationProposal;
private _allocationRequestData: AllocationRequest;
private _endInvestorOnboardingOfferData: EndInvestorOnboardingOfferData;
private _cantonCommandActAs: string[];

constructor(coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -59,6 +61,10 @@ export class Transaction extends BaseTransaction {
this._allocationRequestData = data;
}

set endInvestorOnboardingOfferData(data: EndInvestorOnboardingOfferData) {
this._endInvestorOnboardingOfferData = data;
}

set cantonCommandActAs(parties: string[]) {
this._cantonCommandActAs = parties;
}
Expand Down Expand Up @@ -123,6 +129,17 @@ export class Transaction extends BaseTransaction {
};
return Buffer.from(JSON.stringify(minData)).toString('base64');
}
if (this._type === TransactionType.EndInvestorOnboardingOffer) {
if (!this._endInvestorOnboardingOfferData) {
throw new InvalidTransactionError('EndInvestorOnboardingOfferData is not set');
}
const minData: TransactionBroadcastData = {
txType: TransactionType[this._type],
submissionId: this.id,
endInvestorOnboardingOfferData: this._endInvestorOnboardingOfferData,
};
return Buffer.from(JSON.stringify(minData)).toString('base64');
}
if (!this._prepareCommand) {
throw new InvalidTransactionError('Empty transaction data');
}
Expand Down Expand Up @@ -206,6 +223,13 @@ export class Transaction extends BaseTransaction {
result.allocationRequestData = this._allocationRequestData;
return result;
}
if (this._type === TransactionType.EndInvestorOnboardingOffer) {
if (!this._endInvestorOnboardingOfferData) {
throw new InvalidTransactionError('EndInvestorOnboardingOfferData is not set');
}
result.endInvestorOnboardingOfferData = this._endInvestorOnboardingOfferData;
return result;
}
if (this._type === TransactionType.CantonCommand) {
if (!this._prepareCommand?.preparedTransaction) {
throw new InvalidTransactionError('Empty transaction data');
Expand Down Expand Up @@ -242,7 +266,8 @@ export class Transaction extends BaseTransaction {
if (
this._type === TransactionType.TransferAcknowledge ||
this._type === TransactionType.CosignDelegationProposal ||
this._type === TransactionType.AllocationRequest
this._type === TransactionType.AllocationRequest ||
this._type === TransactionType.EndInvestorOnboardingOffer
) {
return Buffer.from(DUMMY_HASH, 'base64');
}
Expand Down Expand Up @@ -273,6 +298,10 @@ export class Transaction extends BaseTransaction {
if (decoded.allocationRequestData) {
this.allocationRequestData = decoded.allocationRequestData;
}
} else if (this.type === TransactionType.EndInvestorOnboardingOffer) {
if (decoded.endInvestorOnboardingOfferData) {
this.endInvestorOnboardingOfferData = decoded.endInvestorOnboardingOfferData;
}
} else {
if (decoded.prepareCommandResponse) {
this.prepareCommand = decoded.prepareCommandResponse;
Expand Down Expand Up @@ -398,6 +427,21 @@ export class Transaction extends BaseTransaction {
}
return explanation;
}
case TransactionType.EndInvestorOnboardingOffer: {
// Non-signable notification record — no inputs, outputs, or amounts to explain.
return {
id: this.id,
displayOrder,
outputs: [],
outputAmount: '0',
inputs: [],
inputAmount: '0',
changeOutputs: [],
changeAmount: '0',
fee: { fee: '0' },
type: this.type,
};
}
}
return {
id: this.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AllocationAllocateWithdrawnBuilder } from './allocationAllocateWithdraw
import { AllocationRejectBuilder } from './allocationRejectBuilder';
import { AllocationRequestBuilder } from './allocationRequestBuilder';
import { CantonCommandBuilder } from './cantonCommandBuilder';
import { EndInvestorOnboardingOfferBuilder } from './endInvestorOnboardingOfferBuilder';
import { CosignDelegationAcceptBuilder } from './cosignDelegationAcceptBuilder';
import { CosignDelegationProposalBuilder } from './cosignDelegationProposalBuilder';
import { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
Expand Down Expand Up @@ -76,6 +77,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
case TransactionType.CantonCommand: {
return this.getCantonCommandBuilder(tx);
}
case TransactionType.EndInvestorOnboardingOffer: {
return this.getEndInvestorOnboardingOfferBuilder(tx);
}
default: {
throw new InvalidTransactionError('unsupported transaction');
}
Expand Down Expand Up @@ -103,6 +107,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return TransactionBuilderFactory.initializeBuilder(tx, new CantonCommandBuilder(this._coinConfig));
}

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

getOneStepPreapprovalBuilder(tx?: Transaction): OneStepPreApprovalBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new OneStepPreApprovalBuilder(this._coinConfig));
}
Expand Down
Loading
Loading