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
19 changes: 17 additions & 2 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,36 @@ export class Canton extends BaseCoin {
/** @inheritDoc */
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
const coinConfig = coins.get(this.getChain());
// extract `txParams` when verifying other transaction types
const { txPrebuild: txPrebuild } = params;
const { txPrebuild: txPrebuild, txParams } = params;
const rawTx = txPrebuild.txHex;
if (!rawTx) {
throw new Error('missing required tx prebuild property txHex');
}
const txBuilder = new TransactionBuilderFactory(coinConfig).from(rawTx);
const transaction = txBuilder.transaction;
const explainedTx = transaction.explainTransaction();
switch (transaction.type) {
case TransactionType.WalletInitialization:
case TransactionType.TransferAccept:
case TransactionType.TransferReject:
case TransactionType.TransferAcknowledge:
// There is no input for these type of transactions, so always return true.
return true;
case TransactionType.Send:
if (txParams.recipients !== undefined) {
const filteredRecipients = txParams.recipients?.map((recipient) => {
const { address, amount } = recipient;
return { address, amount };
});
const filteredOutputs = explainedTx.outputs?.map((output) => {
const { address, amount } = output;
return { address, amount };
});
if (JSON.stringify(filteredRecipients) !== JSON.stringify(filteredOutputs)) {
throw new Error('Tx outputs do not match with expected txParams recipients');
}
}
return true;
default: {
throw new Error(`unknown transaction type, ${transaction.type}`);
}
Expand Down
16 changes: 16 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ export interface WalletInitTxData {
preparedParty: PreparedParty;
}

export interface UTXOInfo {
contractId: string;
value: string;
}

export interface CantonPrepareCommandResponse {
preparedTransaction?: string;
preparedTransactionHash: string;
hashingSchemeVersion: string;
hashingDetails?: string | null;
utxoInfo?: UTXOInfo[];
}

export interface PreparedParty {
Expand Down Expand Up @@ -133,3 +139,13 @@ export interface TransferAcknowledge {
expiryEpoch: number;
updateId: string;
}

export interface CantonTransferRequest {
commandId: string;
senderPartyId: string;
receiverPartyId: string;
amount: number;
expiryEpoch: number;
sendViaOneStep: boolean;
memoId?: string;
}
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 { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
export { Transaction } from './transaction/transaction';
export { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
export { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
export { TransferBuilder } from './transferBuilder';
export { TransactionBuilder } from './transactionBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { TransferRejectionBuilder } from './transferRejectionBuilder';
Expand Down
38 changes: 36 additions & 2 deletions modules/sdk-coin-canton/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BaseKey,
BaseTransaction,
Entry,
InvalidTransactionError,
ITransactionRecipient,
TransactionType,
Expand Down Expand Up @@ -177,6 +178,7 @@ export class Transaction extends BaseTransaction {
if (this.type !== TransactionType.TransferAcknowledge) {
if (decoded.prepareCommandResponse) {
this.prepareCommand = decoded.prepareCommandResponse;
this.loadInputsAndOutputs();
}
if (decoded.partySignatures && decoded.partySignatures.signatures.length > 0) {
this.signerFingerprint = decoded.partySignatures.signatures[0].party.split('::')[1];
Expand All @@ -192,6 +194,30 @@ export class Transaction extends BaseTransaction {
}
}

/**
* Loads the input & output fields for the transaction
*
*/
loadInputsAndOutputs(): void {
const outputs: Entry[] = [];
const inputs: Entry[] = [];
const txData = this.toJson();
const input: Entry = {
address: txData.sender,
value: txData.amount,
coin: this._coinConfig.name,
};
const output: Entry = {
address: txData.receiver,
value: txData.amount,
coin: this._coinConfig.name,
};
inputs.push(input);
outputs.push(output);
this._inputs = inputs;
this._outputs = outputs;
}

explainTransaction(): TransactionExplanation {
const displayOrder = [
'id',
Expand All @@ -205,7 +231,9 @@ export class Transaction extends BaseTransaction {
'type',
];
const inputs: ITransactionRecipient[] = [];
const outputs: ITransactionRecipient[] = [];
let inputAmount = '0';
let outputAmount = '0';
switch (this.type) {
case TransactionType.TransferAccept:
case TransactionType.TransferReject: {
Expand All @@ -214,12 +242,18 @@ export class Transaction extends BaseTransaction {
inputAmount = txData.amount;
break;
}
case TransactionType.Send: {
const txData = this.toJson();
outputs.push({ address: txData.sender, amount: txData.amount });
outputAmount = txData.amount;
break;
}
}
return {
id: this.id,
displayOrder,
outputs: [],
outputAmount: '0',
outputs: outputs,
outputAmount: outputAmount,
inputs: inputs,
inputAmount: inputAmount,
changeOutputs: [],
Expand Down
166 changes: 162 additions & 4 deletions modules/sdk-coin-canton/src/lib/transferBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,181 @@
import { PublicKey, TransactionType } from '@bitgo/sdk-core';
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionBuilder } from './transactionBuilder';
import { CantonPrepareCommandResponse } from './iface';
import { Transaction } from './transaction/transaction';
import { CantonPrepareCommandResponse, CantonTransferRequest } from './iface';
import utils from './utils';

export class TransferBuilder extends TransactionBuilder {
private _commandId: string;
private _senderId: string;
private _receiverId: string;
private _amount: number;
private _sendOneStep = false;
private _expiryEpoch: number;
private _memoId: string;
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

protected get transactionType(): TransactionType {
initBuilder(tx: Transaction): void {
super.initBuilder(tx);
this.setTransactionType();
}

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

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

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

/** @inheritDoc */
addSignature(publicKey: PublicKey, signature: Buffer): void {
throw new Error('Not implemented');
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
* 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 sender party id for the transfer
* @param id - sender address (party id)
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
senderId(id: string): this {
if (!id || !id.trim()) {
throw new Error('senderId must be a non-empty string');
}
this._senderId = id.trim();
return this;
}

/**
* Sets the receiver party id for the transfer
* @param id - receiver address (party id)
* @returns The current builder instance for chaining.
* @throws Error if id is empty.
*/
receiverId(id: string): this {
if (!id || !id.trim()) {
throw new Error('receiverId must be a non-empty string');
}
this._receiverId = id.trim();
return this;
}

/**
* Sets the transfer amount
* @param amount - transfer amount
* @returns The current builder instance for chaining.
* @throws Error if amount not present or negative
*/
amount(amount: number): this {
if (!amount || amount < 0) {
throw new Error('amount must be a positive number');
}
this._amount = amount;
return this;
}

/**
* Sets the 1-step enablement flag to send via 1-step, works only if recipient
* enabled the 1-step, defaults to `false`
* @param flag boolean value
* @returns The current builder for chaining
*/
sendOneStep(flag: boolean): this {
this._sendOneStep = flag;
return this;
}

/**
* Sets the transfer expiry
* @param epoch - the expiry for 2-step transfer, defaults to 90 days and
* not applicable if sending via 1-step
* @returns The current builder for chaining
* @throws Error if the expiry value is invalid
*/
expiryEpoch(epoch: number): this {
if (!epoch || epoch < 0) {
throw new Error('epoch must be a positive number');
}
this._expiryEpoch = epoch;
return this;
}

/**
* Sets the optional memoId if present
* @param id - memoId of the recipient
* @returns The current builder for chaining
* @throws Error if the memoId value is invalid
*/
memoId(id: string): this {
if (!id || !id.trim()) {
throw new Error('memoId must be a non-empty string');
}
this._memoId = id.trim();
return this;
}

/**
* Get the canton transfer request object
* @returns CantonTransferRequest
* @throws Error if any required params are missing
*/
toRequestObject(): CantonTransferRequest {
this.validate();
const data: CantonTransferRequest = {
commandId: this._commandId,
senderPartyId: this._senderId,
receiverPartyId: this._receiverId,
amount: this._amount,
expiryEpoch: this._expiryEpoch,
sendViaOneStep: this._sendOneStep,
};
if (this._memoId) {
data.memoId = this._memoId;
}
return data;
}

/**
* Method to validate the required fields
* @throws Error if required fields are not set
* @private
*/
private validate(): void {
if (!this._commandId) throw new Error('commandId is missing');
if (!this._senderId) throw new Error('senderId is missing');
if (!this._receiverId) throw new Error('receiverId is missing');
if (!this._amount) throw new Error('amount is missing');
if (!this._expiryEpoch) throw new Error('expiryEpoch is missing');
}
}
17 changes: 17 additions & 0 deletions modules/sdk-coin-canton/test/resources.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('Wallet Pre-approval Enablement Builder', () => {
const txBuilder = new OneStepPreApprovalBuilder(coins.get('tcanton'));
const oneStepEnablementTx = new Transaction(coins.get('tcanton'));
txBuilder.initBuilder(oneStepEnablementTx);
txBuilder.setTransaction(OneStepPreApprovalPrepareResponse);
const { commandId, partyId } = OneStepEnablement;
txBuilder.commandId(commandId).receiverPartyId(partyId);
const requestObj: CantonOneStepEnablementRequest = txBuilder.toRequestObject();
Expand Down
Loading