Skip to content

Commit

Permalink
feat(sdk-coin-icp): transaction build added for ICP
Browse files Browse the repository at this point in the history
TICKET: WIN-4248
  • Loading branch information
mohd-kashif committed Jan 31, 2025
1 parent fa245c4 commit d67e95c
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 82 deletions.
3 changes: 2 additions & 1 deletion modules/sdk-coin-icp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"@dfinity/candid": "^2.2.0",
"@dfinity/principal": "^2.2.0",
"crc-32": "^1.2.2",
"elliptic": "^6.6.1"
"elliptic": "^6.6.1",
"superagent": "^10.1.1"
},
"devDependencies": {
"@bitgo/sdk-api": "^1.58.5",
Expand Down
8 changes: 4 additions & 4 deletions modules/sdk-coin-icp/src/icp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@bitgo/sdk-core';
import { KeyPair as IcpKeyPair } from './lib/keyPair';
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import utils from './lib/utils';
import { Utils } from './lib/utils';

/**
* Class representing the Internet Computer (ICP) coin.
Expand Down Expand Up @@ -95,7 +95,7 @@ export class Icp extends BaseCoin {
}

isValidPub(key: string): boolean {
return utils.isValidPublicKey(key);
return Utils.isValidPublicKey(key);
}

isValidPrv(_: string): boolean {
Expand All @@ -117,13 +117,13 @@ export class Icp extends BaseCoin {
if (!isKeyValid) {
throw new Error('Public Key is not in a valid Hex Encoded Format');
}
const compressedKey = utils.compressPublicKey(hexEncodedPublicKey);
const compressedKey = Utils.compressPublicKey(hexEncodedPublicKey);
const KeyPair = new IcpKeyPair({ pub: compressedKey });
return KeyPair.getAddress();
}

/** @inheritDoc **/
protected getPublicNodeUrl(): string {
public getPublicNodeUrl(): string {
return Environments[this.bitgo.getEnv()].rosettaNodeURL;
}
}
93 changes: 93 additions & 0 deletions modules/sdk-coin-icp/src/lib/iface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
export interface IcpTransactionData {
readonly senderAddress: string;
readonly receiverAddress: string;
readonly amount: string;
readonly fee: string;
readonly senderPublicKeyHex: string;
readonly sequenceNumber: number;
readonly transactionType: string;
readonly expireTime: number;
readonly coin: string;
readonly id: string;
}

export enum IcpTransactionType {
Transfer = 'Transfer',
}

export interface IcpNetworkIdentifier {
blockchain: string;
network: string;
}

export interface IcpPublicKey {
hex_bytes: string;
curve_type: string;
}

export interface IcpOperationIdentifier {
index: number;
}

export interface IcpAccount {
address: string;
}

export interface IcpCurrency {
symbol: string;
decimals: number;
}

export interface IcpAmount {
value: string;
currency: IcpCurrency;
}

export interface IcpOperation {
operation_identifier: IcpOperationIdentifier;
type: string;
account: IcpAccount;
amount: IcpAmount;
}

export interface IcpMetadata {
created_at_time: number;
memo: number;
ingress_start: number;
ingress_end: number;
}

export interface IcpTransaction {
network_identifier: IcpNetworkIdentifier;
public_keys: IcpPublicKey[];
operations: IcpOperation[];
metadata: IcpMetadata;
}

export interface IcpUnsignedTransaction {
unsigned_transaction: string;
payloads: IcpPayload[];
}

export interface IcpPayload {
account_identifier: IcpAccountIdentifier;
hex_bytes: string;
signature_type: string;
}

export interface IcpAccountIdentifier {
address: string;
}

export interface IcpSignature {
signing_payload: IcpPayload;
signature_type: string;
public_key: IcpPublicKey;
hex_bytes: string;
}

export interface IcpCombineApiPayload {
network_identifier: IcpNetworkIdentifier;
unsigned_transaction: string;
signatures: IcpSignature[];
}
112 changes: 112 additions & 0 deletions modules/sdk-coin-icp/src/lib/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { BaseKey, BaseTransaction } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { IcpTransactionData, IcpTransaction, IcpMetadata } from './iface';
import { Utils } from './utils';

export class Transaction extends BaseTransaction {
protected IcpTransaction: IcpTransaction;
protected IcpTransactionData: IcpTransactionData;
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

get icpTransaction(): IcpTransaction {
return this.IcpTransaction;
}

setIcpTransaction(tx: IcpTransaction): void {
this.IcpTransaction = tx;
}

/** @inheritdoc */
toJson(): IcpTransaction {
if (!this.IcpTransactionData) {
throw new Error('Transaction data is not set.');
}

this.IcpTransaction = {
network_identifier: Utils.getNetworkIdentifier(),
public_keys: [
{
hex_bytes: this.IcpTransactionData.senderPublicKeyHex,
curve_type: Utils.getCurveType(),
},
],
operations: [
{
operation_identifier: { index: 0 },
type: Utils.getTransactionType(),
account: { address: this.IcpTransactionData.senderAddress },
amount: {
value: `-${this.IcpTransactionData.amount}`, // Negative for sender
currency: { symbol: this.IcpTransactionData.coin, decimals: Utils.getDecimalPrecision() },
},
},
{
operation_identifier: { index: 1 },
type: Utils.getTransactionType(),
account: { address: this.IcpTransactionData.receiverAddress },
amount: {
value: this.IcpTransactionData.amount, // Positive for receiver
currency: { symbol: this.IcpTransactionData.coin, decimals: Utils.getDecimalPrecision() },
},
},
{
operation_identifier: { index: 2 },
type: Utils.getFeeType(),
account: { address: this.IcpTransactionData.senderAddress },
amount: {
value: `-${this.IcpTransactionData.fee}`, // FEE is negative
currency: { symbol: this.IcpTransactionData.coin, decimals: Utils.getDecimalPrecision() },
},
},
],
metadata: this.getMetaData(),
};

return this.IcpTransaction;
}

getMetaData(): IcpMetadata {
const currentTime = Date.now() * 1_000_000;
if (this.IcpTransactionData.expireTime >= currentTime) {
throw new Error('Invalid expire time');
}
return {
created_at_time: currentTime,
memo: this.IcpTransactionData.sequenceNumber,
ingress_start: currentTime,
ingress_end: this.IcpTransactionData.expireTime,
};
}

/** @inheritdoc */
toBroadcastFormat() {
throw new Error('Method not implemented.');
}

/** @inheritdoc */
canSign(key: BaseKey): boolean {
return true;
}

static parseRawTransaction(rawTransaction: string): IcpTransactionData {
try {
const parsedTx = JSON.parse(rawTransaction);
return {
senderAddress: parsedTx.address,
receiverAddress: parsedTx.externalOutputs[0].address,
amount: parsedTx.inputAmount,
fee: parsedTx.fee, //TODO: check if this is correct
senderPublicKeyHex: parsedTx.senderPublicKey,
sequenceNumber: parsedTx.seqno,
transactionType: parsedTx.type,
expireTime: parsedTx.expireTime,
id: parsedTx.id,
coin: parsedTx.coin,
};
} catch (error) {
throw new Error('Invalid raw transaction format: ' + error.message);
}
}
}
24 changes: 10 additions & 14 deletions modules/sdk-coin-icp/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import {
BaseKey,
BaseTransaction,
PublicKey as BasePublicKey,
BaseTransactionBuilder,
BaseAddress,
Recipient,
} from '@bitgo/sdk-core';
import { TransferBuilder } from './transferBuilder';
import { BaseKey, BaseTransaction, BaseTransactionBuilder, BaseAddress, Recipient } from '@bitgo/sdk-core';
import { Transaction } from './transaction';
import { IcpCombineApiPayload, IcpUnsignedTransaction } from './iface';

export abstract class TransactionBuilder extends BaseTransactionBuilder {
protected _transfer: TransferBuilder;
protected _transaction: Transaction;
protected _unsignedTransaction: IcpUnsignedTransaction;
protected _combineApiPayload: IcpCombineApiPayload;

protected constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
Expand All @@ -22,11 +18,11 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
}

/**
* add a signature to the transaction
* Initialize the transaction builder fields using the decoded transaction data
*
* @param {Transaction} tx the transaction data
*/
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
throw new Error('method not implemented');
}
abstract initBuilder(tx: Transaction): void;

/**
* Sets the sender of this transaction.
Expand Down
63 changes: 38 additions & 25 deletions modules/sdk-coin-icp/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
import { BaseTransactionBuilderFactory } from '@bitgo/sdk-core';
import { BaseTransactionBuilderFactory, InvalidTransactionError } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { TransferTransaction } from './transferTransaction';
import { IcpTransactionData, IcpTransactionType } from './iface';
import { TransferBuilder } from './transferBuilder';
import utils from './utils';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

/** @inheritdoc */
from(): void {
throw new Error('method not implemented');
}

/** @inheritdoc */
getTransferBuilder(): void {
throw new Error('method not implemented');
from(rawTransaction: string): TransactionBuilder {
utils.validateRawTransaction(rawTransaction);
const transaction = this.parseTransaction(rawTransaction);
try {
switch (transaction.transactionType) {
case IcpTransactionType.Transfer:
const transferTx = new TransferTransaction(this._coinConfig);
transferTx.fromRawTransaction(rawTransaction);
return this.getTransferBuilder(transferTx);
default:
throw new InvalidTransactionError('Invalid transaction');
}
} catch (e) {
throw new InvalidTransactionError('Invalid transaction: ' + e.message);
}
}

/** @inheritdoc */
getStakingBuilder(): void {
throw new Error('method not implemented');
}

/** @inheritdoc */
getUnstakingBuilder(): void {
throw new Error('method not implemented');
}

/** @inheritdoc */
getCustomTransactionBuilder(): void {
throw new Error('method not implemented');
/**
* Initialize the builder with the given transaction
*
* @param {Transaction | undefined} tx - the transaction used to initialize the builder
* @param {TransactionBuilder} builder - the builder to be initialized
* @returns {TransactionBuilder} the builder initialized
*/
private initializeBuilder<T extends TransactionBuilder>(tx: Transaction | undefined, builder: T): T {
if (tx) {
builder.initBuilder(tx);
}
return builder;
}

/** @inheritdoc */
getTokenTransferBuilder(): void {
throw new Error('method not implemented');
getTransferBuilder(tx?: Transaction): TransferBuilder {
return this.initializeBuilder(tx, new TransferBuilder(this._coinConfig));
}

/** @inheritdoc */
Expand All @@ -44,7 +57,7 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
/**
* Parse the transaction from a raw transaction
*/
private parseTransaction(rawTransaction: string): void {
throw new Error('method not implemented');
private parseTransaction(rawTransaction: string): IcpTransactionData {
return Transaction.parseRawTransaction(rawTransaction);
}
}
Loading

0 comments on commit d67e95c

Please sign in to comment.