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 @@ -113,6 +113,7 @@ export class Canton extends BaseCoin {
case TransactionType.TransferReject:
case TransactionType.TransferAcknowledge:
case TransactionType.OneStepPreApproval:
case TransactionType.TransferOfferWithdrawn:
// There is no input for these type of transactions, so always return true.
return true;
case TransactionType.Send:
Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,17 @@ export interface TransactionBroadcastData {

export interface CantonOneStepEnablementRequest extends CantonPrepareCommandRequest {
receiverId: string;
token?: string;
}

export interface CantonTransferAcceptRejectRequest extends CantonPrepareCommandRequest {
contractId: string;
}

export interface CantonTransferOfferWithdrawnRequest extends CantonTransferAcceptRejectRequest {
tokenName?: string;
}

export interface TransferAcknowledge {
contractId: string;
senderPartyId: string;
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 @@ -9,6 +9,7 @@ export { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
export { TransferBuilder } from './transferBuilder';
export { TransactionBuilder } from './transactionBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { TransferOfferWithdrawnBuilder } from './transferOfferWithdrawnBuilder';
export { TransferRejectionBuilder } from './transferRejectionBuilder';
export { WalletInitBuilder } from './walletInitBuilder';
export { WalletInitTransaction } from './walletInitialization/walletInitTransaction';
Expand Down
16 changes: 16 additions & 0 deletions modules/sdk-coin-canton/src/lib/oneStepPreApprovalBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import utils from './utils';
export class OneStepPreApprovalBuilder extends TransactionBuilder {
private _commandId: string;
private _receiverPartyId: string;
private _token: string;
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}
Expand Down Expand Up @@ -73,6 +74,20 @@ export class OneStepPreApprovalBuilder extends TransactionBuilder {
return this;
}

/**
* Sets the optional token field if present, used for canton token preApproval setup
* @param name - the bitgo name of the token
* @returns The current builder for chaining
* @throws Error if name is invalid
*/
token(name: string): this {
if (!name || !name.trim()) {
throw new Error('token name must be a non-empty string');
}
this._token = name.trim();
return this;
}

/**
* Builds and returns the CantonOneStepEnablementRequest object from the builder's internal state.
*
Expand All @@ -91,6 +106,7 @@ export class OneStepPreApprovalBuilder extends TransactionBuilder {
verboseHashing: false,
actAs: [this._receiverPartyId],
readAs: [],
token: this._token,
};
}

Expand Down
22 changes: 21 additions & 1 deletion modules/sdk-coin-canton/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,14 @@ export class Transaction extends BaseTransaction {
case TransactionType.TransferAccept:
case TransactionType.TransferReject: {
const txData = this.toJson();
inputs.push({ address: txData.sender, amount: txData.amount });
const input: ITransactionRecipient = {
address: txData.sender,
amount: txData.amount,
};
if (txData.token) {
input.tokenName = txData.token;
}
inputs.push(input);
inputAmount = txData.amount;
break;
}
Expand All @@ -264,6 +271,19 @@ export class Transaction extends BaseTransaction {
outputAmount = txData.amount;
break;
}
case TransactionType.TransferOfferWithdrawn: {
const txData = this.toJson();
const input: ITransactionRecipient = {
address: txData.receiver,
amount: txData.amount,
};
if (txData.token) {
input.tokenName = txData.token;
}
inputs.push(input);
inputAmount = txData.amount;
break;
}
}
return {
id: this.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
import { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
import { TransactionBuilder } from './transactionBuilder';
import { TransferBuilder } from './transferBuilder';
import { TransferOfferWithdrawnBuilder } from './transferOfferWithdrawnBuilder';
import { TransferRejectionBuilder } from './transferRejectionBuilder';
import { Transaction } from './transaction/transaction';
import { WalletInitBuilder } from './walletInitBuilder';
Expand Down Expand Up @@ -41,6 +42,9 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
case TransactionType.TransferAcknowledge: {
return this.getTransferAcknowledgeBuilder(tx);
}
case TransactionType.TransferOfferWithdrawn: {
return this.getTransferOfferWithdrawnBuilder(tx);
}
case TransactionType.TransferReject: {
return this.getTransferRejectBuilder(tx);
}
Expand All @@ -63,6 +67,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return TransactionBuilderFactory.initializeBuilder(tx, new TransferAcknowledgeBuilder(this._coinConfig));
}

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

getTransferRejectBuilder(tx?: Transaction): TransferRejectionBuilder {
return TransactionBuilderFactory.initializeBuilder(tx, new TransferRejectionBuilder(this._coinConfig));
}
Expand Down
139 changes: 139 additions & 0 deletions modules/sdk-coin-canton/src/lib/transferOfferWithdrawnBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CantonPrepareCommandResponse, CantonTransferOfferWithdrawnRequest } from './iface';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction/transaction';
import utils from './utils';

export class TransferOfferWithdrawnBuilder extends TransactionBuilder {
private _commandId: string;
private _contractId: string;
private _actAsPartyId: string;
private _tokenName: string;
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

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

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

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

setTransaction(transaction: CantonPrepareCommandResponse): void {
this.transaction.prepareCommand = transaction;
}

/** @inheritDoc */
addSignature(publicKey: PublicKey, signature: Buffer): void {
if (!this.transaction) {
throw new InvalidTransactionError('transaction is empty!');
}
this._signatures.push({ publicKey, signature });
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
this.transaction.signatures = signature.toString('base64');
}

/**
* Sets the unique id for the transfer offer withdrawn
* Also sets the _id of the transaction
*
* @param id - A uuid
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
commandId(id: string): this {
if (!id || !id.trim()) {
throw new Error('commandId must be a non-empty string');
}
this._commandId = id.trim();
// also set the transaction _id
this.transaction.id = id.trim();
return this;
}

/**
* Sets the contract id the receiver needs to withdraw
* @param id - canton withdrawn contract id
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
contractId(id: string): this {
if (!id || !id.trim()) {
throw new Error('contractId must be a non-empty string');
}
this._contractId = id.trim();
return this;
}

/**
* The sender who wants to withdraw the offer
*
* @param id - the sender party id
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
actAs(id: string): this {
if (!id || !id.trim()) {
throw new Error('actAsPartyId must be a non-empty string');
}
this._actAsPartyId = id.trim();
return this;
}

/**
* The token name to withdraw the offer
* @param name - the bitgo name of the asset
* @returns The current builder instance for chaining.
* @throws Error if name is empty.
*/
tokenName(name: string): this {
if (!name || !name.trim()) {
throw new Error('tokenName must be a non-empty string');
}
this._tokenName = name.trim();
return this;
}

/**
* Builds and returns the CantonTransferOfferWithdrawnRequest object from the builder's internal state.
*
* This method performs validation before constructing the object. If required fields are
* missing or invalid, it throws an error.
*
* @returns {CantonTransferOfferWithdrawnRequest} - A fully constructed and validated request object for transfer offer withdrawal.
* @throws {Error} If any required field is missing or fails validation.
*/
toRequestObject(): CantonTransferOfferWithdrawnRequest {
this.validate();

return {
commandId: this._commandId,
contractId: this._contractId,
verboseHashing: false,
actAs: [this._actAsPartyId],
readAs: [],
tokenName: this._tokenName,
};
}

/**
* Validates the internal state of the builder before building the request object.
*
* @private
* @throws {Error} If any required field is missing or invalid.
*/
private validate(): void {
if (!this._commandId) throw new Error('commandId is missing');
if (!this._contractId) throw new Error('contractId is missing');
if (!this._actAsPartyId) throw new Error('receiver partyId is missing');
}
}
81 changes: 81 additions & 0 deletions modules/sdk-coin-canton/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,12 @@ export class Utils implements BaseUtils {
let instrumentAdmin: string | undefined;
let token: string | undefined;
let preApprovalNode: RecordField[] = [];
let tokenPreApprovalNode: RecordField[] = [];
let transferNode: RecordField[] = [];
let transferAcceptRejectNode: RecordField[] = [];
let tokenTransferAcceptRejectNode: RecordField[] = [];
let withdrawnNode: RecordField[] = [];
let tokenWithdrawnNode: RecordField[] = [];
const nodes = decodedData.transaction?.nodes;

nodes?.forEach((node) => {
Expand All @@ -122,6 +125,13 @@ export class Utils implements BaseUtils {
) {
preApprovalNode = fields;
}
if (
template?.entityName === 'TransferPreapproval' &&
!tokenPreApprovalNode.length &&
txType === TransactionType.OneStepPreApproval
) {
tokenPreApprovalNode = fields;
}
if (
template?.entityName === 'Amulet' &&
!transferAcceptRejectNode.length &&
Expand All @@ -140,6 +150,20 @@ export class Utils implements BaseUtils {
tokenTransferAcceptRejectNode = transferSum.record?.fields ?? [];
}
}
if (
template?.entityName === 'Amulet' &&
!withdrawnNode.length &&
txType === TransactionType.TransferOfferWithdrawn
) {
withdrawnNode = fields;
}
if (
template?.entityName === 'Holding' &&
!tokenWithdrawnNode.length &&
txType === TransactionType.TransferOfferWithdrawn
) {
tokenWithdrawnNode = fields;
}
});

nodes?.forEach((node) => {
Expand All @@ -166,6 +190,27 @@ export class Utils implements BaseUtils {
const providerData = getField(preApprovalNode, 'provider');
if (providerData?.oneofKind === 'party') sender = providerData.party ?? '';
amount = '0';
} else if (tokenPreApprovalNode.length) {
const receiverData = getField(tokenPreApprovalNode, 'receiver');
if (receiverData?.oneofKind === 'party') receiver = receiverData.party ?? '';
const operatorData = getField(tokenPreApprovalNode, 'operator');
if (operatorData?.oneofKind === 'party') sender = operatorData.party ?? '';
amount = '0';
const instrumentAdminData = getField(tokenPreApprovalNode, 'instrumentAdmin');
if (instrumentAdminData?.oneofKind === 'party') instrumentAdmin = instrumentAdminData.party ?? '';
const allowancesData = getField(tokenPreApprovalNode, 'instrumentAllowances');
if (allowancesData?.oneofKind === 'list') {
// for the same instrument admin, if multiple tokens are supported then we can enable all of them,
// but we won't be doing that for now
const firstAllowance = allowancesData.list?.elements?.[0]?.sum;
if (firstAllowance?.oneofKind === 'record') {
const allowanceFields = firstAllowance.record?.fields ?? [];
const idData = getField(allowanceFields, 'id');
if (idData?.oneofKind === 'text') {
instrumentId = idData.text ?? '';
}
}
}
} else if (transferNode.length) {
const transferField = transferNode.find((f) => f.label === 'transfer');
const transferSum = transferField?.value?.sum;
Expand Down Expand Up @@ -247,6 +292,42 @@ export class Utils implements BaseUtils {
instrumentId = idData.text ?? '';
}
}
} else if (withdrawnNode.length) {
const ownerData = getField(withdrawnNode, 'owner');
if (ownerData?.oneofKind === 'party') {
receiver = ownerData.party ?? '';
sender = receiver;
}
const amountField = getField(withdrawnNode, 'amount');
if (amountField?.oneofKind === 'record') {
const amountFields = amountField.record?.fields ?? [];
const initialAmountData = getField(amountFields, 'initialAmount');
if (initialAmountData?.oneofKind === 'numeric') {
amount = initialAmountData.numeric ?? '';
}
}
} else if (tokenWithdrawnNode.length) {
const ownerData = getField(tokenWithdrawnNode, 'owner');
if (ownerData?.oneofKind === 'party') {
receiver = ownerData.party ?? '';
sender = receiver;
}
const amountData = getField(tokenWithdrawnNode, 'amount');
if (amountData?.oneofKind === 'numeric') {
amount = amountData.numeric ?? '';
}
const instrumentData = getField(tokenWithdrawnNode, 'instrument');
if (instrumentData?.oneofKind === 'record') {
const instrumentFields = instrumentData.record?.fields ?? [];
const adminData = getField(instrumentFields, 'source');
if (adminData?.oneofKind === 'party') {
instrumentAdmin = adminData.party ?? '';
}
const idData = getField(instrumentFields, 'id');
if (idData?.oneofKind === 'text') {
instrumentId = idData.text ?? '';
}
}
}
if (!sender || !receiver || !amount) {
const missingFields: string[] = [];
Expand Down
Loading