-
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.
Browse files
Browse the repository at this point in the history
feat(sdk-coin-icp): added address creation and validation logic
- Loading branch information
Showing
8 changed files
with
234 additions
and
11 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
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,59 @@ | ||
import { | ||
DefaultKeys, | ||
isPrivateKey, | ||
isPublicKey, | ||
isSeed, | ||
KeyPairOptions, | ||
Secp256k1ExtendedKeyPair, | ||
} from '@bitgo/sdk-core'; | ||
import { bip32 } from '@bitgo/utxo-lib'; | ||
import { randomBytes } from 'crypto'; | ||
import utils from './utils'; | ||
|
||
const DEFAULT_SEED_SIZE_BYTES = 32; | ||
|
||
/** | ||
* ICP keys and address management. | ||
*/ | ||
export class KeyPair extends Secp256k1ExtendedKeyPair { | ||
/** | ||
* 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); | ||
if (!source) { | ||
const seed = randomBytes(DEFAULT_SEED_SIZE_BYTES); | ||
this.hdNode = bip32.fromSeed(seed); | ||
} else if (isSeed(source)) { | ||
this.hdNode = bip32.fromSeed(source.seed); | ||
} else if (isPrivateKey(source)) { | ||
super.recordKeysFromPrivateKey(source.prv); | ||
} else if (isPublicKey(source)) { | ||
super.recordKeysFromPublicKey(source.pub); | ||
} else { | ||
throw new Error('Invalid key pair options'); | ||
} | ||
|
||
if (this.hdNode) { | ||
this.keyPair = Secp256k1ExtendedKeyPair.toKeyPair(this.hdNode); | ||
} | ||
} | ||
|
||
/** @inheritdoc */ | ||
getKeys(): DefaultKeys { | ||
return { | ||
pub: this.getPublicKey({ compressed: true }).toString('hex'), | ||
prv: this.getPrivateKey()?.toString('hex'), | ||
}; | ||
} | ||
|
||
/** @inheritdoc */ | ||
getAddress(): string { | ||
const principal = utils.derivePrincipalFromPublicKey(this.getKeys().pub); | ||
const subAccount = new Uint8Array(32); | ||
const accountId = utils.fromPrincipal(principal, subAccount); | ||
return accountId.toString(); | ||
} | ||
} |
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,22 +1,103 @@ | ||
import { BaseUtils } from '@bitgo/sdk-core'; | ||
import elliptic from 'elliptic'; | ||
import { Principal as DfinityPrincipal } from '@dfinity/principal'; | ||
import * as agent from '@dfinity/agent'; | ||
import crypto from 'crypto'; | ||
import crc32 from 'crc-32'; | ||
|
||
const Secp256k1Curve = new elliptic.ec('secp256k1'); | ||
|
||
export class Utils implements BaseUtils { | ||
isValidAddress(address: string): boolean { | ||
throw new Error('Method not implemented.'); | ||
} | ||
|
||
isValidTransactionId(txId: string): boolean { | ||
throw new Error('Method not implemented.'); | ||
} | ||
|
||
isValidPublicKey(key: string): boolean { | ||
throw new Error('Method not implemented.'); | ||
const hexRegex = /^[0-9a-fA-F]+$/; | ||
if (!hexRegex.test(key)) return false; | ||
|
||
const length = key.length; | ||
if (length !== 130) return false; | ||
|
||
return true; | ||
} | ||
|
||
isValidPrivateKey(key: string): boolean { | ||
throw new Error('Method not implemented.'); | ||
} | ||
|
||
isValidSignature(signature: string): boolean { | ||
throw new Error('Method not implemented.'); | ||
} | ||
|
||
isValidBlockId(hash: string): boolean { | ||
throw new Error('Method not implemented.'); | ||
} | ||
|
||
getHeaders(): Record<string, string> { | ||
return { | ||
'Content-Type': 'application/json', | ||
}; | ||
} | ||
|
||
getNetworkIdentifier(): Record<string, string> { | ||
return { | ||
blockchain: 'Internet Computer', | ||
network: '00000000000000020101', | ||
}; | ||
} | ||
|
||
compressPublicKey(uncompressedKey: string): string { | ||
if (!uncompressedKey.startsWith('04')) { | ||
throw new Error('Invalid uncompressed public key format'); | ||
} | ||
const xHex = uncompressedKey.slice(2, 66); | ||
const yHex = uncompressedKey.slice(66); | ||
const y = BigInt(`0x${yHex}`); | ||
const prefix = y % 2n === 0n ? '02' : '03'; | ||
return prefix + xHex; | ||
} | ||
|
||
getCurveType(): string { | ||
return 'secp256k1'; | ||
} | ||
|
||
derivePrincipalFromPublicKey(publicKeyHex: string): DfinityPrincipal { | ||
const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex'); | ||
|
||
try { | ||
const ellipticKey = Secp256k1Curve.keyFromPublic(publicKeyBuffer); | ||
const uncompressedPublicKeyHex = ellipticKey.getPublic(false, 'hex'); | ||
const derEncodedKey = agent.wrapDER(Buffer.from(uncompressedPublicKeyHex, 'hex'), agent.SECP256K1_OID); | ||
const principalId = DfinityPrincipal.selfAuthenticating(Buffer.from(derEncodedKey)); | ||
const principal = DfinityPrincipal.fromUint8Array(principalId.toUint8Array()); | ||
return principal; | ||
} catch (error) { | ||
throw new Error(`Failed to process the public key: ${error.message}`); | ||
} | ||
} | ||
|
||
fromPrincipal(principal: DfinityPrincipal, subAccount: Uint8Array = new Uint8Array(32)): string { | ||
const ACCOUNT_ID_PREFIX = new Uint8Array([0x0a, ...Buffer.from('account-id')]); | ||
const principalBytes = principal.toUint8Array(); | ||
const combinedBytes = new Uint8Array(ACCOUNT_ID_PREFIX.length + principalBytes.length + subAccount.length); | ||
|
||
combinedBytes.set(ACCOUNT_ID_PREFIX, 0); | ||
combinedBytes.set(principalBytes, ACCOUNT_ID_PREFIX.length); | ||
combinedBytes.set(subAccount, ACCOUNT_ID_PREFIX.length + principalBytes.length); | ||
|
||
const sha224Hash = crypto.createHash('sha224').update(combinedBytes).digest(); | ||
const checksum = Buffer.alloc(4); | ||
checksum.writeUInt32BE(crc32.buf(sha224Hash) >>> 0, 0); | ||
|
||
const accountIdBytes = Buffer.concat([checksum, sha224Hash]); | ||
return accountIdBytes.toString('hex'); | ||
} | ||
} | ||
|
||
const utils = new Utils(); | ||
export default 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
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
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