Skip to content
Draft
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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"dependencies": {
"@bitgo/sdk-core": "^36.10.1",
"@bitgo/statics": "^58.2.0",
"@canton-network/wallet-sdk": "^0.10.0",
"bignumber.js": "^9.1.1"
},
"devDependencies": {
Expand Down
14 changes: 12 additions & 2 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
} from '@bitgo/sdk-core';
import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc';
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { KeyPair as CantonKeyPair } from './lib/keyPair';
import utils from './lib/utils';

export class Canton extends BaseCoin {
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
Expand Down Expand Up @@ -84,12 +86,20 @@ export class Canton extends BaseCoin {

/** @inheritDoc */
generateKeyPair(seed?: Buffer): KeyPair {
throw new Error('Method not implemented.');
const keyPair = seed ? new CantonKeyPair({ seed }) : new CantonKeyPair();
const keys = keyPair.getKeys();
if (!keys.prv) {
throw new Error('Missing prv in key generation.');
}
return {
pub: keys.pub,
prv: keys.prv,
};
}

/** @inheritDoc */
isValidPub(pub: string): boolean {
throw new Error('Method not implemented.');
return utils.isValidPublicKey(pub);
}

/** @inheritDoc */
Expand Down
32 changes: 32 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import { TransactionType } from '@bitgo/sdk-core';
import { PartyId } from '@canton-network/core-types';

/**
* The transaction data returned from the toJson() function of a transaction
*/
export interface TxData {
id: string;
type: TransactionType;
sender: string;
receiver: string;
}

export interface PreparedTrxParsedInfo {
sender: string;
receiver: string;
amount: string;
}

export interface WalletInitializationDataTxData {
id: string;
type: TransactionType;
}

export interface CantonPrepareCommandResponse {
preparedTransaction?: string;
preparedTransactionHash: string;
hashingSchemeVersion: string;
hashingDetails?: string;
}

export interface PreparedParty {
partyTransactions: Uint8Array<ArrayBufferLike>[];
combinedHash: string;
txHashes: Buffer<ArrayBuffer>[];
namespace: string;
partyId: PartyId;
}
26 changes: 22 additions & 4 deletions modules/sdk-coin-canton/src/lib/keyPair.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
import { DefaultKeys, Ed25519KeyPair } from '@bitgo/sdk-core';
import { DefaultKeys, Ed25519KeyPair, KeyPairOptions } from '@bitgo/sdk-core';
import utils from './utils';

export class KeyPair extends Ed25519KeyPair {
/**
* Public constructor. By default, creates a key pair with a random master seed.
*
* @param { KeyPairOptions } source Either a master seed, a private key, or a public key
*/
constructor(source?: KeyPairOptions) {
super(source);
}

/** @inheritdoc */
getKeys(): DefaultKeys {
throw new Error('Method not implemented.');
const result: DefaultKeys = { pub: this.keyPair.pub };
if (this.keyPair.prv) {
result.prv = this.keyPair.prv;
}
return result;
}

/** @inheritdoc */
recordKeysFromPrivateKeyInProtocolFormat(prv: string): DefaultKeys {
// We don't use private keys for CANTON since it's implemented for TSS.
throw new Error('Method not implemented.');
}

/** @inheritdoc */
recordKeysFromPublicKeyInProtocolFormat(pub: string): DefaultKeys {
throw new Error('Method not implemented.');
if (!utils.isValidPublicKey(pub)) {
throw new Error(`Invalid public key ${pub}`);
}
return { pub };
}

/** @inheritdoc */
getAddress(): string {
throw new Error('Method not implemented.');
return utils.getAddressFromPublicKey(this.keyPair.pub);
}
}
41 changes: 37 additions & 4 deletions modules/sdk-coin-canton/src/lib/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import { BaseKey, BaseTransaction } from '@bitgo/sdk-core';
import { TxData } from './iface';
import { BaseKey, BaseTransaction, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CantonPrepareCommandResponse, TxData } from './iface';
import utils from './utils';

export class Transaction extends BaseTransaction {
private _transaction: CantonPrepareCommandResponse;

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

get transaction(): CantonPrepareCommandResponse {
return this._transaction;
}

set transaction(transaction: CantonPrepareCommandResponse) {
this._transaction = transaction;
this._id = transaction.preparedTransactionHash;
}

canSign(key: BaseKey): boolean {
return false;
}

toBroadcastFormat(): string {
throw new Error('Method not implemented.');
if (!this._transaction) {
throw new InvalidTransactionError('Empty transaction data');
}
return this._transaction.preparedTransactionHash;
}

toJson(): TxData {
throw new Error('Method not implemented.');
if (!this._transaction || !this._transaction.preparedTransaction) {
throw new InvalidTransactionError('Empty transaction data');
}
const result: TxData = {
id: this.id,
type: this._type as TransactionType,
sender: '',
receiver: '',
};
// TODO: extract other required data (utxo used, request time, execute before etc)
const parsedInfo = utils.parseRawCantonTransactionData(this._transaction.preparedTransaction);
result.sender = parsedInfo.sender;
result.receiver = parsedInfo.receiver;
return result;
}
}
92 changes: 90 additions & 2 deletions modules/sdk-coin-canton/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { BaseUtils } from '@bitgo/sdk-core';
import { BaseUtils, isValidEd25519PublicKey } from '@bitgo/sdk-core';
import { RecordField } from '@canton-network/core-ledger-proto';
import { decodePreparedTransaction, TopologyController } from '@canton-network/wallet-sdk';
import { PreparedTrxParsedInfo } from './iface';

export class Utils implements BaseUtils {
/** @inheritdoc */
Expand All @@ -18,7 +21,7 @@ export class Utils implements BaseUtils {

/** @inheritdoc */
isValidPublicKey(key: string): boolean {
throw new Error('Method not implemented.');
return isValidEd25519PublicKey(key);
}

/** @inheritdoc */
Expand All @@ -30,6 +33,91 @@ export class Utils implements BaseUtils {
isValidTransactionId(txId: string): boolean {
throw new Error('Method not implemented.');
}

/**
* Method to create fingerprint (part of the canton partyId) from public key
* @param {String} publicKey the public key
* @returns {String}
*/
getAddressFromPublicKey(publicKey: string): string {
return TopologyController.createFingerprintFromPublicKey(publicKey);
}

/**
* Method to parse raw canton transaction & get required data
* @param {String} rawData base64 encoded string
* @returns {PreparedTrxParsedInfo}
*/
parseRawCantonTransactionData(rawData: string): PreparedTrxParsedInfo {
const decodedData = decodePreparedTransaction(rawData);
let sender = '';
let receiver = '';
let amount = '';
decodedData.transaction?.nodes?.map((node) => {
const versionedNode = node.versionedNode;
if (!versionedNode || versionedNode.oneofKind !== 'v1') return;

const v1Node = versionedNode.v1;
const nodeType = v1Node.nodeType;

if (nodeType.oneofKind !== 'create') return;

const createNode = nodeType.create;

// Check if it's the correct template
const template = createNode.templateId;
if (template?.entityName !== 'AmuletTransferInstruction') return;

// Now parse the 'create' argument
if (createNode.argument?.sum?.oneofKind !== 'record') return;
const fields = createNode.argument?.sum?.record?.fields;
if (!fields) return;

// Find the 'transfer' field
const transferField = fields.find((f) => f.label === 'transfer');
if (transferField?.value?.sum?.oneofKind !== 'record') return;
const transferRecord = transferField?.value?.sum?.record?.fields;
if (!transferRecord) return;

const getField = (fields: RecordField[], label: string) => fields.find((f) => f.label === label)?.value?.sum;

const senderData = getField(transferRecord, 'sender');
if (!senderData || senderData.oneofKind !== 'party') return;
sender = senderData.party;
const receiverData = getField(transferRecord, 'receiver');
if (!receiverData || receiverData.oneofKind !== 'party') return;
receiver = receiverData.party;
const amountData = getField(transferRecord, 'amount');
if (!amountData || amountData.oneofKind !== 'numeric') return;
amount = amountData.numeric;
});
if (!sender || !receiver || !amount) {
throw new Error('invalid transaction data');
}
return {
sender,
receiver,
amount,
};
}

/**
* Method to compute the prepared transaction hash offline
* @param {String} preparedTransaction base64 encoded prepared transaction
* @returns {Promise<string>} the computed hash
*/
async computeHashFromPreparedTransaction(preparedTransaction: string): Promise<string> {
return await TopologyController.createTransactionHash(preparedTransaction);
}

/**
* Method to compute the topology transactions hash offline
* @param {Uint8Array<ArrayBufferLike>[]} preparedTransactions the prepared topology transactions
* @returns {Promise<String>} the computed hash
*/
async computeHashFromTopologyTransaction(preparedTransactions: Uint8Array<ArrayBufferLike>[]): Promise<string> {
return await TopologyController.computeTopologyTxHash(preparedTransactions);
}
}

const utils = new Utils();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BaseKey, BaseTransaction, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { PreparedParty, WalletInitializationDataTxData } from '../iface';

export class Transaction extends BaseTransaction {
private _transaction: PreparedParty;

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

get transaction(): PreparedParty {
return this._transaction;
}

set transaction(transaction: PreparedParty) {
this._transaction = transaction;
this._id = transaction.combinedHash;
}

canSign(key: BaseKey): boolean {
return false;
}

toBroadcastFormat(): string {
if (!this._transaction) {
throw new InvalidTransactionError('Empty transaction data');
}
return this._transaction.combinedHash;
}

toJson(): WalletInitializationDataTxData {
if (!this._transaction) {
throw new InvalidTransactionError('Empty transaction data');
}
const result: WalletInitializationDataTxData = {
id: this.id,
type: this._type as TransactionType,
};
// Add logic to parse the preparedTransaction & extract sender, receiver
return result;
}
}
Loading