-
Notifications
You must be signed in to change notification settings - Fork 286
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sdk-coin-icp): implemented transaction builder and validations f…
…or ICP TICKET: WIN-4635
- Loading branch information
1 parent
4785f16
commit ea4cc9f
Showing
16 changed files
with
1,964 additions
and
194 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.