Skip to content

Commit

Permalink
feat(sdk-coin-icp): implemented transaction builder and validations f…
Browse files Browse the repository at this point in the history
…or ICP

TICKET: WIN-4635
  • Loading branch information
mohd-kashif committed Feb 27, 2025
1 parent 4785f16 commit ea4cc9f
Show file tree
Hide file tree
Showing 16 changed files with 1,964 additions and 194 deletions.
8 changes: 6 additions & 2 deletions modules/sdk-coin-icp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@
"@dfinity/agent": "^2.2.0",
"@dfinity/candid": "^2.2.0",
"@dfinity/principal": "^2.2.0",
"@noble/curves": "1.8.1",
"crc-32": "^1.2.2"
"bignumber.js": "^9.1.1",
"cbor-x": "^1.6.0",
"crc-32": "^1.2.2",
"js-sha256": "^0.11.0",
"protobufjs": "^7.4.0",
"@noble/curves": "1.8.1"
},
"devDependencies": {
"@bitgo/sdk-api": "^1.58.9",
Expand Down
186 changes: 186 additions & 0 deletions modules/sdk-coin-icp/src/lib/iface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import {
TransactionExplanation as BaseTransactionExplanation,
TransactionType as BitGoTransactionType,
} from '@bitgo/sdk-core';

export enum RequestType {
CALL = 'call',
READ_STATE = 'read_state',
}

export enum SignatureType {
ECDSA = 'ecdsa',
}

export enum CurveType {
SECP256K1 = 'secp256k1',
}

export enum OperationType {
TRANSACTION = 'TRANSACTION',
FEE = 'FEE',
}

export enum MethodName {
SEND_PB = 'send_pb', // send_pb is the method name for ICP transfer transaction
}

export enum NetworkID {
MAINNET = '00000000000000020101', // ICP does not have different network IDs for mainnet and testnet
}

export interface IcpTransactionData {
senderAddress: string;
receiverAddress: string;
amount: string;
fee: string;
senderPublicKeyHex: string;
memo: number | BigInt; // memo in string is not accepted by ICP chain.
transactionType: OperationType;
expiryTime: number | BigInt;
}

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

export interface IcpAccount {
address: string;
}

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

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

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

export interface IcpMetadata {
created_at_time: number;
memo: number | BigInt; // memo in string is not accepted by ICP chain.
ingress_start: number | BigInt; // it should be nano seconds
ingress_end: number | BigInt; // it should be nano seconds
}

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

export interface IcpAccountIdentifier {
address: string;
}

export interface SendArgs {
memo: { memo: number | BigInt };
payment: { receiverGets: { e8s: number } };
maxFee: { e8s: number };
to: { hash: Buffer };
createdAtTime: { timestampNanos: number };
}

export interface HttpCanisterUpdate {
canister_id: Uint8Array;
method_name: MethodName;
arg: Uint8Array;
sender: Uint8Array;
ingress_expiry: bigint;
}

export interface SigningPayload {
account_identifier: IcpAccountIdentifier;
hex_bytes: string;
signature_type: SignatureType;
}

export interface PayloadsData {
payloads: SigningPayload[];
unsigned_transaction: string;
}

export interface Signatures {
signing_payload: SigningPayload;
signature_type: SignatureType;
public_key: IcpPublicKey;
hex_bytes: string;
}

export interface cborUnsignedTransaction {
updates: [string, HttpCanisterUpdate][];
ingress_expiries: bigint[];
}

export interface ReadState {
sender: Uint8Array;
paths: Array<[Buffer, Buffer]>;
ingress_expiry: bigint;
}

export interface UpdateEnvelope {
content: {
request_type: RequestType;
canister_id: Uint8Array;
method_name: MethodName;
arg: Uint8Array;
sender: Uint8Array;
ingress_expiry: bigint;
};
sender_pubkey: Uint8Array;
sender_sig: Uint8Array;
}

export interface ReadStateEnvelope {
content: {
request_type: RequestType;
sender: Uint8Array;
paths: Array<[Uint8Array, Uint8Array]>;
ingress_expiry: bigint;
};
sender_pubkey: Uint8Array;
sender_sig: Uint8Array;
}

export interface RequestEnvelope {
update: UpdateEnvelope;
read_state: ReadStateEnvelope;
}

/**
* The transaction data returned from the toJson() function of a transaction
*/
export interface TxData {
id?: string;
sender: string;
senderPublicKey: string;
recipient: string;
memo: number | BigInt;
feeAmount: string;
expirationTime: number | BigInt;
type?: BitGoTransactionType;
}

export interface IcpTransactionExplanation extends BaseTransactionExplanation {
sender?: string;
type?: BitGoTransactionType;
}

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

export interface SignedTransactionRequest {
network_identifier: NetworkIdentifier;
signed_transaction: string;
}
3 changes: 3 additions & 0 deletions modules/sdk-coin-icp/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as Utils from './utils';

export { KeyPair } from './keyPair';
export { TransactionBuilder } from './transactionBuilder';
export { TransferBuilder } from './transferBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { Transaction } from './transaction';
export { Utils };
39 changes: 39 additions & 0 deletions modules/sdk-coin-icp/src/lib/message.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
syntax = "proto3";

message Memo {
uint64 memo = 1;
}

message Tokens {
uint64 e8s = 1;
}

message Payment {
Tokens receiver_gets = 1;
}

message Subaccount {
bytes sub_account = 1;
}

message AccountIdentifier {
bytes hash = 1;
}

message BlockIndex {
uint64 height = 1;
}

message TimeStamp {
uint64 timestamp_nanos = 1;
}

message SendRequest {
Memo memo = 1;
Payment payment = 2;
Tokens max_fee = 3;
Subaccount from_subaccount = 4;
AccountIdentifier to = 5;
BlockIndex created_at = 6;
TimeStamp created_at_time = 7;
}
93 changes: 93 additions & 0 deletions modules/sdk-coin-icp/src/lib/signedTransactionBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
cborUnsignedTransaction,
RequestType,
Signatures,
UpdateEnvelope,
ReadStateEnvelope,
RequestEnvelope,
} from './iface';
import utils from './utils';
import assert from 'assert';

export class SignedTransactionBuilder {
protected _unsigned_transaction: string;
protected _signaturePayload: Signatures[];

constructor(unsigned_transaction: string, signatures: Signatures[]) {
this._unsigned_transaction = unsigned_transaction;
this._signaturePayload = signatures;
}

getSignTransaction(): string {
const combineRequest = {
signatures: this._signaturePayload,
unsigned_transaction: this._unsigned_transaction,
};
const signatureMap = new Map();
for (const sig of combineRequest.signatures) {
signatureMap.set(sig.signing_payload.hex_bytes, sig);
}
/*{
string: SIGNATURE
}
*/
const unsignedTransaction = utils.cborDecode(
utils.blobFromHex(combineRequest.unsigned_transaction)
) as cborUnsignedTransaction;
assert(combineRequest.signatures.length === unsignedTransaction.ingress_expiries.length * 2);
assert(unsignedTransaction.updates.length === 1);
const envelopes = this.getEnvelopes(unsignedTransaction, signatureMap);
const envelopRequests = { requests: envelopes };
const signedTransaction = utils.blobToHex(Buffer.from(utils.cborEncode(envelopRequests)));
return signedTransaction;
}

getEnvelopes(
unsignedTransaction: cborUnsignedTransaction,
signatureMap: Map<string, Signatures>
): [string, RequestEnvelope[]][] {
const envelopes: [string, RequestEnvelope[]][] = [];
for (const [reqType, update] of unsignedTransaction.updates) {
const requestEnvelopes: RequestEnvelope[] = [];
for (const ingressExpiry of unsignedTransaction.ingress_expiries) {
update.ingress_expiry = ingressExpiry;

const readState = utils.makeReadStateFromUpdate(update);

const transaction_signature = signatureMap.get(
utils.blobToHex(utils.makeSignatureData(utils.generateHttpCanisterUpdateId(update)))
);
if (!transaction_signature) {
throw new Error('Transaction signature is undefined');
}

const readStateSignature = signatureMap.get(
utils.blobToHex(utils.makeSignatureData(utils.HttpReadStateRepresentationIndependentHash(readState)))
);
if (!readStateSignature) {
throw new Error('read state signature is undefined');
}

const pk_der = utils.getPublicKeyInDERFormat(transaction_signature.public_key.hex_bytes);
const updateEnvelope: UpdateEnvelope = {
content: { request_type: RequestType.CALL, ...update },
sender_pubkey: pk_der,
sender_sig: utils.blobFromHex(transaction_signature.hex_bytes),
};

const readStateEnvelope: ReadStateEnvelope = {
content: { request_type: RequestType.READ_STATE, ...readState },
sender_pubkey: pk_der,
sender_sig: utils.blobFromHex(readStateSignature.hex_bytes),
};

requestEnvelopes.push({
update: updateEnvelope,
read_state: readStateEnvelope,
});
}
envelopes.push([reqType, requestEnvelopes]);
}
return envelopes;
}
}
Loading

0 comments on commit ea4cc9f

Please sign in to comment.