Skip to content

Commit 1b9a1ed

Browse files
committed
feat: transfer builder added
Ticket: COIN-6381
1 parent 110c0bf commit 1b9a1ed

File tree

8 files changed

+314
-8
lines changed

8 files changed

+314
-8
lines changed

modules/sdk-coin-canton/src/canton.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,21 +98,36 @@ export class Canton extends BaseCoin {
9898
/** @inheritDoc */
9999
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
100100
const coinConfig = coins.get(this.getChain());
101-
// extract `txParams` when verifying other transaction types
102-
const { txPrebuild: txPrebuild } = params;
101+
const { txPrebuild: txPrebuild, txParams } = params;
103102
const rawTx = txPrebuild.txHex;
104103
if (!rawTx) {
105104
throw new Error('missing required tx prebuild property txHex');
106105
}
107106
const txBuilder = new TransactionBuilderFactory(coinConfig).from(rawTx);
108107
const transaction = txBuilder.transaction;
108+
const explainedTx = transaction.explainTransaction();
109109
switch (transaction.type) {
110110
case TransactionType.WalletInitialization:
111111
case TransactionType.TransferAccept:
112112
case TransactionType.TransferReject:
113113
case TransactionType.TransferAcknowledge:
114114
// There is no input for these type of transactions, so always return true.
115115
return true;
116+
case TransactionType.Send:
117+
if (txParams.recipients !== undefined) {
118+
const filteredRecipients = txParams.recipients?.map((recipient) => {
119+
const { address, amount } = recipient;
120+
return { address, amount };
121+
});
122+
const filteredOutputs = explainedTx.outputs?.map((output) => {
123+
const { address, amount } = output;
124+
return { address, amount };
125+
});
126+
if (JSON.stringify(filteredRecipients) !== JSON.stringify(filteredOutputs)) {
127+
throw new Error('Tx outputs do not match with expected txParams recipients');
128+
}
129+
}
130+
return true;
116131
default: {
117132
throw new Error(`unknown transaction type, ${transaction.type}`);
118133
}

modules/sdk-coin-canton/src/lib/iface.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ export interface WalletInitTxData {
3535
preparedParty: PreparedParty;
3636
}
3737

38+
export interface UTXOInfo {
39+
contractId: string;
40+
value: string;
41+
}
42+
3843
export interface CantonPrepareCommandResponse {
3944
preparedTransaction?: string;
4045
preparedTransactionHash: string;
4146
hashingSchemeVersion: string;
4247
hashingDetails?: string | null;
48+
utxoInfo?: UTXOInfo[];
4349
}
4450

4551
export interface PreparedParty {
@@ -133,3 +139,13 @@ export interface TransferAcknowledge {
133139
expiryEpoch: number;
134140
updateId: string;
135141
}
142+
143+
export interface CantonTransferRequest {
144+
commandId: string;
145+
senderPartyId: string;
146+
receiverPartyId: string;
147+
amount: number;
148+
expiryEpoch: number;
149+
sendViaOneStep: boolean;
150+
memoId?: string;
151+
}

modules/sdk-coin-canton/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
66
export { Transaction } from './transaction/transaction';
77
export { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
88
export { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
9+
export { TransferBuilder } from './transferBuilder';
910
export { TransactionBuilder } from './transactionBuilder';
1011
export { TransactionBuilderFactory } from './transactionBuilderFactory';
1112
export { TransferRejectionBuilder } from './transferRejectionBuilder';

modules/sdk-coin-canton/src/lib/transaction/transaction.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
BaseKey,
33
BaseTransaction,
4+
Entry,
45
InvalidTransactionError,
56
ITransactionRecipient,
67
TransactionType,
@@ -177,6 +178,7 @@ export class Transaction extends BaseTransaction {
177178
if (this.type !== TransactionType.TransferAcknowledge) {
178179
if (decoded.prepareCommandResponse) {
179180
this.prepareCommand = decoded.prepareCommandResponse;
181+
this.loadInputsAndOutputs();
180182
}
181183
if (decoded.partySignatures && decoded.partySignatures.signatures.length > 0) {
182184
this.signerFingerprint = decoded.partySignatures.signatures[0].party.split('::')[1];
@@ -192,6 +194,30 @@ export class Transaction extends BaseTransaction {
192194
}
193195
}
194196

197+
/**
198+
* Loads the input & output fields for the transaction
199+
*
200+
*/
201+
loadInputsAndOutputs(): void {
202+
const outputs: Entry[] = [];
203+
const inputs: Entry[] = [];
204+
const txData = this.toJson();
205+
const input: Entry = {
206+
address: txData.sender,
207+
value: txData.amount,
208+
coin: this._coinConfig.name,
209+
};
210+
const output: Entry = {
211+
address: txData.receiver,
212+
value: txData.amount,
213+
coin: this._coinConfig.name,
214+
};
215+
inputs.push(input);
216+
outputs.push(output);
217+
this._inputs = inputs;
218+
this._outputs = outputs;
219+
}
220+
195221
explainTransaction(): TransactionExplanation {
196222
const displayOrder = [
197223
'id',
@@ -205,7 +231,9 @@ export class Transaction extends BaseTransaction {
205231
'type',
206232
];
207233
const inputs: ITransactionRecipient[] = [];
234+
const outputs: ITransactionRecipient[] = [];
208235
let inputAmount = '0';
236+
let outputAmount = '0';
209237
switch (this.type) {
210238
case TransactionType.TransferAccept:
211239
case TransactionType.TransferReject: {
@@ -214,12 +242,18 @@ export class Transaction extends BaseTransaction {
214242
inputAmount = txData.amount;
215243
break;
216244
}
245+
case TransactionType.Send: {
246+
const txData = this.toJson();
247+
outputs.push({ address: txData.sender, amount: txData.amount });
248+
outputAmount = txData.amount;
249+
break;
250+
}
217251
}
218252
return {
219253
id: this.id,
220254
displayOrder,
221-
outputs: [],
222-
outputAmount: '0',
255+
outputs: outputs,
256+
outputAmount: outputAmount,
223257
inputs: inputs,
224258
inputAmount: inputAmount,
225259
changeOutputs: [],
Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,181 @@
1-
import { PublicKey, TransactionType } from '@bitgo/sdk-core';
1+
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { TransactionBuilder } from './transactionBuilder';
4-
import { CantonPrepareCommandResponse } from './iface';
4+
import { Transaction } from './transaction/transaction';
5+
import { CantonPrepareCommandResponse, CantonTransferRequest } from './iface';
6+
import utils from './utils';
57

68
export class TransferBuilder extends TransactionBuilder {
9+
private _commandId: string;
10+
private _senderId: string;
11+
private _receiverId: string;
12+
private _amount: number;
13+
private _sendOneStep = false;
14+
private _expiryEpoch: number;
15+
private _memoId: string;
716
constructor(_coinConfig: Readonly<CoinConfig>) {
817
super(_coinConfig);
918
}
1019

11-
protected get transactionType(): TransactionType {
20+
initBuilder(tx: Transaction): void {
21+
super.initBuilder(tx);
22+
this.setTransactionType();
23+
}
24+
25+
get transactionType(): TransactionType {
1226
return TransactionType.Send;
1327
}
1428

29+
setTransactionType(): void {
30+
this.transaction.transactionType = TransactionType.Send;
31+
}
32+
1533
setTransaction(transaction: CantonPrepareCommandResponse): void {
1634
this.transaction.prepareCommand = transaction;
1735
}
1836

1937
/** @inheritDoc */
2038
addSignature(publicKey: PublicKey, signature: Buffer): void {
21-
throw new Error('Not implemented');
39+
if (!this.transaction) {
40+
throw new InvalidTransactionError('transaction is empty!');
41+
}
42+
this._signatures.push({ publicKey, signature });
43+
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
44+
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
45+
this.transaction.signatures = signature.toString('base64');
46+
}
47+
48+
/**
49+
* Sets the unique id for the transfer
50+
* Also sets the _id of the transaction
51+
*
52+
* @param id - A uuid
53+
* @returns The current builder instance for chaining.
54+
* @throws Error if id is empty.
55+
*/
56+
commandId(id: string): this {
57+
if (!id || !id.trim()) {
58+
throw new Error('commandId must be a non-empty string');
59+
}
60+
this._commandId = id.trim();
61+
// also set the transaction _id
62+
this.transaction.id = id.trim();
63+
return this;
64+
}
65+
66+
/**
67+
* Sets the sender party id for the transfer
68+
* @param id - sender address (party id)
69+
* @returns The current builder instance for chaining.
70+
* @throws Error if id is empty.
71+
*/
72+
senderId(id: string): this {
73+
if (!id || !id.trim()) {
74+
throw new Error('senderId must be a non-empty string');
75+
}
76+
this._senderId = id.trim();
77+
return this;
78+
}
79+
80+
/**
81+
* Sets the receiver party id for the transfer
82+
* @param id - receiver address (party id)
83+
* @returns The current builder instance for chaining.
84+
* @throws Error if id is empty.
85+
*/
86+
receiverId(id: string): this {
87+
if (!id || !id.trim()) {
88+
throw new Error('receiverId must be a non-empty string');
89+
}
90+
this._receiverId = id.trim();
91+
return this;
92+
}
93+
94+
/**
95+
* Sets the transfer amount
96+
* @param amount - transfer amount
97+
* @returns The current builder instance for chaining.
98+
* @throws Error if amount not present or negative
99+
*/
100+
amount(amount: number): this {
101+
if (!amount || amount < 0) {
102+
throw new Error('amount must be a positive number');
103+
}
104+
this._amount = amount;
105+
return this;
106+
}
107+
108+
/**
109+
* Sets the 1-step enablement flag to send via 1-step, works only if recipient
110+
* enabled the 1-step, defaults to `false`
111+
* @param flag boolean value
112+
* @returns The current builder for chaining
113+
*/
114+
sendOneStep(flag: boolean): this {
115+
this._sendOneStep = flag;
116+
return this;
117+
}
118+
119+
/**
120+
* Sets the transfer expiry
121+
* @param epoch - the expiry for 2-step transfer, defaults to 90 days and
122+
* not applicable if sending via 1-step
123+
* @returns The current builder for chaining
124+
* @throws Error if the expiry value is invalid
125+
*/
126+
expiryEpoch(epoch: number): this {
127+
if (!epoch || epoch < 0) {
128+
throw new Error('epoch must be a positive number');
129+
}
130+
this._expiryEpoch = epoch;
131+
return this;
132+
}
133+
134+
/**
135+
* Sets the optional memoId if present
136+
* @param id - memoId of the recipient
137+
* @returns The current builder for chaining
138+
* @throws Error if the memoId value is invalid
139+
*/
140+
memoId(id: string): this {
141+
if (!id || !id.trim()) {
142+
throw new Error('memoId must be a non-empty string');
143+
}
144+
this._memoId = id.trim();
145+
return this;
146+
}
147+
148+
/**
149+
* Get the canton transfer request object
150+
* @returns CantonTransferRequest
151+
* @throws Error if any required params are missing
152+
*/
153+
toRequestObject(): CantonTransferRequest {
154+
this.validate();
155+
const data: CantonTransferRequest = {
156+
commandId: this._commandId,
157+
senderPartyId: this._senderId,
158+
receiverPartyId: this._receiverId,
159+
amount: this._amount,
160+
expiryEpoch: this._expiryEpoch,
161+
sendViaOneStep: this._sendOneStep,
162+
};
163+
if (this._memoId) {
164+
data.memoId = this._memoId;
165+
}
166+
return data;
167+
}
168+
169+
/**
170+
* Method to validate the required fields
171+
* @throws Error if required fields are not set
172+
* @private
173+
*/
174+
private validate(): void {
175+
if (!this._commandId) throw new Error('commandId is missing');
176+
if (!this._senderId) throw new Error('senderId is missing');
177+
if (!this._receiverId) throw new Error('receiverId is missing');
178+
if (!this._amount) throw new Error('amount is missing');
179+
if (!this._expiryEpoch) throw new Error('expiryEpoch is missing');
22180
}
23181
}

modules/sdk-coin-canton/test/resources.ts

Lines changed: 17 additions & 0 deletions
Large diffs are not rendered by default.

modules/sdk-coin-canton/test/unit/builder/oneStepEnablement/oneStepEnablementBuilder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('Wallet Pre-approval Enablement Builder', () => {
1717
const txBuilder = new OneStepPreApprovalBuilder(coins.get('tcanton'));
1818
const oneStepEnablementTx = new Transaction(coins.get('tcanton'));
1919
txBuilder.initBuilder(oneStepEnablementTx);
20+
txBuilder.setTransaction(OneStepPreApprovalPrepareResponse);
2021
const { commandId, partyId } = OneStepEnablement;
2122
txBuilder.commandId(commandId).receiverPartyId(partyId);
2223
const requestObj: CantonOneStepEnablementRequest = txBuilder.toRequestObject();

0 commit comments

Comments
 (0)