Skip to content

Commit 6ef5e3f

Browse files
authored
Merge pull request #7345 from BitGo/COIN-6180
feat(sdk-coin-canton): added transfer acknowledgement builder
2 parents b671837 + a51a9f6 commit 6ef5e3f

File tree

15 files changed

+365
-17
lines changed

15 files changed

+365
-17
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ export const PUBLIC_KEY_FORMAT = 'CRYPTO_KEY_FORMAT_RAW';
1414
export const PUBLIC_KEY_SPEC = 'SIGNING_KEY_SPEC_EC_CURVE25519';
1515
export const SIGNATURE_FORMAT = 'SIGNATURE_FORMAT_RAW';
1616
export const SIGNATURE_ALGORITHM_SPEC = 'SIGNING_ALGORITHM_SPEC_ED25519';
17+
export const HASHING_SCHEME_VERSION = 'HASHING_SCHEME_VERSION_V2';
18+
19+
export const DUMMY_HASH = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,42 @@ export interface WalletInitBroadcastData {
8181
multiHashSignatures: MultiHashSignature[];
8282
}
8383

84+
export interface PartySignature {
85+
party: string;
86+
signatures: MultiHashSignature[];
87+
}
88+
89+
export interface TransactionBroadcastData {
90+
prepareCommandResponse?: CantonPrepareCommandResponse;
91+
txType: string;
92+
preparedTransaction?: string;
93+
partySignatures?: {
94+
signatures: PartySignature[];
95+
};
96+
deduplicationPeriod?: {
97+
Empty: Record<string, never>;
98+
};
99+
submissionId: string;
100+
hashingSchemeVersion?: string;
101+
minLedgerTime?: {
102+
time: {
103+
Empty: Record<string, never>;
104+
};
105+
};
106+
}
107+
84108
export interface CantonOneStepEnablementRequest extends CantonPrepareCommandRequest {
85109
receiverId: string;
86110
}
87111

88112
export interface CantonTransferAcceptRequest extends CantonPrepareCommandRequest {
89113
contractId: string;
90114
}
115+
116+
export interface TransferAcknowledge {
117+
contractId: string;
118+
senderPartyId: string;
119+
amount: number;
120+
expiryEpoch: number;
121+
updateId: string;
122+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { KeyPair } from './keyPair';
55
export { OneStepPreApprovalBuilder } from './oneStepPreApprovalBuilder';
66
export { Transaction } from './transaction/transaction';
77
export { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
8+
export { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
89
export { TransactionBuilder } from './transactionBuilder';
910
export { TransactionBuilderFactory } from './transactionBuilderFactory';
1011
export { WalletInitBuilder } from './walletInitBuilder';

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { TransactionType } from '@bitgo/sdk-core';
1+
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { CantonPrepareCommandResponse, CantonOneStepEnablementRequest } from './iface';
44
import { TransactionBuilder } from './transactionBuilder';
55
import { Transaction } from './transaction/transaction';
6+
import utils from './utils';
67

78
export class OneStepPreApprovalBuilder extends TransactionBuilder {
89
private _commandId: string;
@@ -28,6 +29,17 @@ export class OneStepPreApprovalBuilder extends TransactionBuilder {
2829
this.transaction.prepareCommand = transaction;
2930
}
3031

32+
/** @inheritDoc */
33+
addSignature(publicKey: PublicKey, signature: Buffer): void {
34+
if (!this.transaction) {
35+
throw new InvalidTransactionError('transaction is empty!');
36+
}
37+
this._signatures.push({ publicKey, signature });
38+
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
39+
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
40+
this.transaction.signatures = signature.toString('base64');
41+
}
42+
3143
/**
3244
* Sets the unique id for the 1-step enablement
3345
* Also sets the _id of the transaction

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

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { BaseKey, BaseTransaction, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3-
import { CantonPrepareCommandResponse, PreparedTxnParsedInfo, TxData } from '../iface';
3+
import {
4+
CantonPrepareCommandResponse,
5+
MultiHashSignature,
6+
PartySignature,
7+
PreparedTxnParsedInfo,
8+
TransactionBroadcastData,
9+
TxData,
10+
} from '../iface';
411
import utils from '../utils';
12+
import { DUMMY_HASH, HASHING_SCHEME_VERSION, SIGNATURE_ALGORITHM_SPEC, SIGNATURE_FORMAT } from '../constant';
513

614
export class Transaction extends BaseTransaction {
715
private _prepareCommand: CantonPrepareCommandResponse;
16+
private _signerFingerprint: string;
817

918
constructor(coinConfig: Readonly<CoinConfig>) {
1019
super(coinConfig);
@@ -37,23 +46,84 @@ export class Transaction extends BaseTransaction {
3746
return false;
3847
}
3948

49+
set signatures(signature: string) {
50+
this._signatures.push(signature);
51+
}
52+
53+
set signerFingerprint(fingerprint: string) {
54+
this._signerFingerprint = fingerprint;
55+
}
56+
4057
toBroadcastFormat(): string {
58+
if (!this._type) {
59+
throw new InvalidTransactionError('Transaction type is not set');
60+
}
61+
if (this._type === TransactionType.TransferAcknowledge) {
62+
const minData: TransactionBroadcastData = {
63+
txType: TransactionType[this._type],
64+
submissionId: this.id,
65+
};
66+
return Buffer.from(JSON.stringify(minData)).toString('base64');
67+
}
4168
if (!this._prepareCommand) {
4269
throw new InvalidTransactionError('Empty transaction data');
4370
}
44-
return Buffer.from(JSON.stringify(this._prepareCommand)).toString('base64');
71+
const partySignatures: PartySignature[] = [];
72+
const data: TransactionBroadcastData = {
73+
prepareCommandResponse: this._prepareCommand,
74+
txType: this._type ? TransactionType[this._type] : '',
75+
preparedTransaction: '',
76+
partySignatures: {
77+
signatures: partySignatures,
78+
},
79+
deduplicationPeriod: {
80+
Empty: {},
81+
},
82+
submissionId: this.id,
83+
hashingSchemeVersion: HASHING_SCHEME_VERSION,
84+
minLedgerTime: {
85+
time: {
86+
Empty: {},
87+
},
88+
},
89+
};
90+
const signatures: MultiHashSignature[] = [];
91+
if (this._signatures.length > 0 && this._signerFingerprint) {
92+
const signerPartyId = `${this._signerFingerprint.slice(0, 5)}::${this._signerFingerprint}`;
93+
this.signature.map((signature) => {
94+
const signatureObj: MultiHashSignature = {
95+
format: SIGNATURE_FORMAT,
96+
signature: signature,
97+
signedBy: this._signerFingerprint,
98+
signingAlgorithmSpec: SIGNATURE_ALGORITHM_SPEC,
99+
};
100+
signatures.push(signatureObj);
101+
});
102+
const partySignature = {
103+
party: signerPartyId,
104+
signatures: signatures,
105+
};
106+
data.partySignatures?.signatures.push(partySignature);
107+
data.preparedTransaction = this._prepareCommand.preparedTransaction
108+
? this._prepareCommand.preparedTransaction
109+
: '';
110+
}
111+
return Buffer.from(JSON.stringify(data)).toString('base64');
45112
}
46113

47114
toJson(): TxData {
48-
if (!this._prepareCommand || !this._prepareCommand.preparedTransaction) {
49-
throw new InvalidTransactionError('Empty transaction data');
50-
}
51115
const result: TxData = {
52116
id: this.id,
53117
type: this._type as TransactionType,
54118
sender: '',
55119
receiver: '',
56120
};
121+
if (this._type === TransactionType.TransferAcknowledge) {
122+
return result;
123+
}
124+
if (!this._prepareCommand || !this._prepareCommand.preparedTransaction) {
125+
throw new InvalidTransactionError('Empty transaction data');
126+
}
57127
// TODO: extract other required data (utxo used, request time, execute before etc)
58128
let parsedInfo: PreparedTxnParsedInfo;
59129
try {
@@ -67,6 +137,9 @@ export class Transaction extends BaseTransaction {
67137
}
68138

69139
get signablePayload(): Buffer {
140+
if (this._type === TransactionType.TransferAcknowledge) {
141+
return Buffer.from(DUMMY_HASH, 'base64');
142+
}
70143
if (!this._prepareCommand) {
71144
throw new InvalidTransactionError('Empty transaction data');
72145
}
@@ -75,8 +148,18 @@ export class Transaction extends BaseTransaction {
75148

76149
fromRawTransaction(rawTx: string): void {
77150
try {
78-
const decoded: CantonPrepareCommandResponse = JSON.parse(Buffer.from(rawTx, 'base64').toString('utf8'));
79-
this.prepareCommand = decoded;
151+
const decoded: TransactionBroadcastData = JSON.parse(Buffer.from(rawTx, 'base64').toString('utf8'));
152+
this.id = decoded.submissionId;
153+
this.transactionType = TransactionType[decoded.txType];
154+
if (this.transactionType !== TransactionType.TransferAcknowledge) {
155+
if (decoded.prepareCommandResponse) {
156+
this.prepareCommand = decoded.prepareCommandResponse;
157+
}
158+
if (decoded.partySignatures && decoded.partySignatures.signatures.length > 0) {
159+
this.signerFingerprint = decoded.partySignatures.signatures[0].party.split('::')[1];
160+
this.signatures = decoded.partySignatures.signatures[0].signatures[0].signature;
161+
}
162+
}
80163
} catch (e) {
81164
throw new InvalidTransactionError('Unable to parse raw transaction data');
82165
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import utils from './utils';
1515

1616
export abstract class TransactionBuilder extends BaseTransactionBuilder {
1717
protected _transaction: Transaction;
18-
private _signatures: Signature[] = [];
18+
protected _signatures: Signature[] = [];
1919

2020
initBuilder(tx: Transaction): void {
2121
this._transaction = tx;
@@ -40,9 +40,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
4040
}
4141

4242
/** @inheritDoc */
43-
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
44-
this._signatures.push({ publicKey, signature });
45-
}
43+
abstract addSignature(publicKey: BasePublicKey, signature: Buffer): void;
4644

4745
/** @inheritdoc */
4846
protected fromImplementation(rawTransaction: string): Transaction {

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@bitgo/sdk-core';
77
import { BaseCoin as CoinConfig } from '@bitgo/statics';
88
import { TransferAcceptanceBuilder } from './transferAcceptanceBuilder';
9+
import { TransferAcknowledgeBuilder } from './transferAcknowledgeBuilder';
910
import { TransactionBuilder } from './transactionBuilder';
1011
import { TransferBuilder } from './transferBuilder';
1112
import { Transaction } from './transaction/transaction';
@@ -32,18 +33,24 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3233
case TransactionType.TransferAccept: {
3334
return this.getTransferAcceptanceBuilder(tx);
3435
}
36+
case TransactionType.TransferAcknowledge: {
37+
return this.getTransferAcknowledgeBuilder(tx);
38+
}
3539
default: {
3640
throw new InvalidTransactionError('unsupported transaction');
3741
}
3842
}
3943
}
4044
}
4145

42-
/** @inheritdoc */
4346
getTransferAcceptanceBuilder(tx?: Transaction): TransferAcceptanceBuilder {
4447
return TransactionBuilderFactory.initializeBuilder(tx, new TransferAcceptanceBuilder(this._coinConfig));
4548
}
4649

50+
getTransferAcknowledgeBuilder(tx?: Transaction): TransferAcknowledgeBuilder {
51+
return TransactionBuilderFactory.initializeBuilder(tx, new TransferAcknowledgeBuilder(this._coinConfig));
52+
}
53+
4754
/** @inheritdoc */
4855
getTransferBuilder(tx?: Transaction): TransferBuilder {
4956
return TransactionBuilderFactory.initializeBuilder(tx, new TransferBuilder(this._coinConfig));

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { TransactionType } from '@bitgo/sdk-core';
1+
import { InvalidTransactionError, PublicKey, TransactionType } from '@bitgo/sdk-core';
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { CantonPrepareCommandResponse, CantonTransferAcceptRequest } from './iface';
44
import { TransactionBuilder } from './transactionBuilder';
55
import { Transaction } from './transaction/transaction';
6+
import utils from './utils';
67

78
export class TransferAcceptanceBuilder extends TransactionBuilder {
89
private _commandId: string;
@@ -29,6 +30,17 @@ export class TransferAcceptanceBuilder extends TransactionBuilder {
2930
this.transaction.prepareCommand = transaction;
3031
}
3132

33+
/** @inheritDoc */
34+
addSignature(publicKey: PublicKey, signature: Buffer): void {
35+
if (!this.transaction) {
36+
throw new InvalidTransactionError('transaction is empty!');
37+
}
38+
this._signatures.push({ publicKey, signature });
39+
const pubKeyBase64 = utils.getBase64FromHex(publicKey.pub);
40+
this.transaction.signerFingerprint = utils.getAddressFromPublicKey(pubKeyBase64);
41+
this.transaction.signatures = signature.toString('base64');
42+
}
43+
3244
/**
3345
* Sets the unique id for the transfer acceptance
3446
* Also sets the _id of the transaction

0 commit comments

Comments
 (0)