Skip to content

Commit e92eec5

Browse files
committed
feat(sdk-coin-xrp): add Utils & KeyPair
Ticket: WIN-3569 BREAKING CHANGE: Move following functions from `XRP` to `Utils`, - `getAddressDetails` - `normalizeAddress`
1 parent 6ab0321 commit e92eec5

File tree

9 files changed

+606
-125
lines changed

9 files changed

+606
-125
lines changed

modules/sdk-coin-xrp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@
4141
},
4242
"dependencies": {
4343
"@bitgo/sdk-core": "^28.8.0",
44+
"@bitgo/statics": "^50.1.0",
4445
"@bitgo/utxo-lib": "^11.0.0",
4546
"bignumber.js": "^9.0.0",
4647
"lodash": "^4.17.14",
47-
"ripple-address-codec": "^5.0.0",
4848
"ripple-binary-codec": "^2.1.0",
4949
"ripple-keypairs": "^2.0.0",
5050
"xrpl": "^4.0.0"

modules/sdk-coin-xrp/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './xrp';
22
export * from './txrp';
33
export * from './register';
44
export * from './lib/iface';
5+
export * from './lib/utils';

modules/sdk-coin-xrp/src/lib/iface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
InitiateRecoveryOptions as BaseInitiateRecoveryOptions,
33
SignTransactionOptions as BaseSignTransactionOptions,
4-
TransactionPrebuild,
54
VerifyAddressOptions as BaseVerifyAddressOptions,
65
TransactionExplanation,
6+
TransactionPrebuild,
77
} from '@bitgo/sdk-core';
88

99
export interface Address {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
DefaultKeys,
3+
isPrivateKey,
4+
isPublicKey,
5+
isSeed,
6+
KeyPairOptions,
7+
Secp256k1ExtendedKeyPair,
8+
} from '@bitgo/sdk-core';
9+
import { bip32 } from '@bitgo/utxo-lib';
10+
import { randomBytes } from 'crypto';
11+
import * as xrpl from 'xrpl';
12+
import utils from './utils';
13+
14+
const DEFAULT_SEED_SIZE_BYTES = 32;
15+
16+
/**
17+
* XRP keys and address management.
18+
*/
19+
export class KeyPair extends Secp256k1ExtendedKeyPair {
20+
/**
21+
* Public constructor. By default, creates a key pair with a random master seed.
22+
*
23+
* @param {KeyPairOptions} source Either a master seed, a private key, or a public key
24+
*/
25+
constructor(source?: KeyPairOptions) {
26+
super(source);
27+
if (!source) {
28+
const seed = randomBytes(DEFAULT_SEED_SIZE_BYTES);
29+
this.hdNode = bip32.fromSeed(seed);
30+
} else if (isSeed(source)) {
31+
this.hdNode = bip32.fromSeed(source.seed);
32+
} else if (isPrivateKey(source)) {
33+
super.recordKeysFromPrivateKey(source.prv);
34+
} else if (isPublicKey(source)) {
35+
super.recordKeysFromPublicKey(source.pub);
36+
} else {
37+
throw new Error('Invalid key pair options');
38+
}
39+
40+
if (this.hdNode) {
41+
this.keyPair = Secp256k1ExtendedKeyPair.toKeyPair(this.hdNode);
42+
}
43+
}
44+
45+
/** @inheritdoc */
46+
getKeys(): DefaultKeys {
47+
return {
48+
pub: this.getPublicKey({ compressed: true }).toString('hex'),
49+
prv: this.getPrivateKey()?.toString('hex'),
50+
};
51+
}
52+
53+
/** @inheritdoc */
54+
getAddress(): string {
55+
return xrpl.deriveAddress(this.getKeys().pub);
56+
}
57+
58+
/**
59+
* Generates a signature for an arbitrary string with the current private key using keccak256
60+
* hashing algorithm. Throws if there is no private key.
61+
*
62+
* @param {string} message to produce a signature for
63+
* @returns {Buffer} The signature as a buffer
64+
*/
65+
signMessage(message: string): Buffer {
66+
const messageToSign = Buffer.from(message).toString('hex');
67+
const { prv } = this.getKeys();
68+
if (!prv) {
69+
throw new Error('Missing private key');
70+
}
71+
const signature = utils.signString(messageToSign, prv);
72+
return Buffer.from(signature, 'hex');
73+
}
74+
75+
/**
76+
* Verifies a message signature using the current public key.
77+
*
78+
* @param {string} message signed
79+
* @param {Buffer} signature to verify
80+
* @returns {boolean} True if the message was signed with the current key pair
81+
*/
82+
verifySignature(message: string, signature: Buffer): boolean {
83+
const messageToVerify = Buffer.from(message).toString('hex');
84+
const pubKey = this.getKeys().pub;
85+
return utils.verifySignature(messageToVerify, pubKey, signature.toString('hex'));
86+
}
87+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { BaseUtils, InvalidAddressError, UtilsError } from '@bitgo/sdk-core';
2+
import * as querystring from 'querystring';
3+
import * as rippleKeypairs from 'ripple-keypairs';
4+
import * as url from 'url';
5+
import * as xrpl from 'xrpl';
6+
import { Address } from './iface';
7+
import { KeyPair as XrpKeyPair } from './keyPair';
8+
9+
class Utils implements BaseUtils {
10+
isValidAddress(address: string): boolean {
11+
try {
12+
const addressDetails = this.getAddressDetails(address);
13+
return address === this.normalizeAddress(addressDetails);
14+
} catch (e) {
15+
return false;
16+
}
17+
}
18+
19+
isValidTransactionId(txId: string): boolean {
20+
return this.isValidHex(txId);
21+
}
22+
23+
isValidPublicKey(key: string): boolean {
24+
try {
25+
new XrpKeyPair({ pub: key });
26+
return true;
27+
} catch {
28+
return false;
29+
}
30+
}
31+
32+
isValidPrivateKey(key: string): boolean {
33+
try {
34+
new XrpKeyPair({ prv: key });
35+
return true;
36+
} catch {
37+
return false;
38+
}
39+
}
40+
41+
isValidSignature(signature: string): boolean {
42+
return this.isValidHex(signature);
43+
}
44+
45+
isValidBlockId(hash: string): boolean {
46+
return this.isValidHex(hash);
47+
}
48+
49+
isValidHex(hex: string): boolean {
50+
return /^([a-fA-F0-9])+$/.test(hex);
51+
}
52+
53+
/**
54+
* Parse an address string into address and destination tag
55+
*/
56+
public getAddressDetails(address: string): Address {
57+
const destinationDetails = url.parse(address);
58+
const destinationAddress = destinationDetails.pathname;
59+
if (!destinationAddress || !xrpl.isValidClassicAddress(destinationAddress)) {
60+
throw new InvalidAddressError(`destination address "${destinationAddress}" is not valid`);
61+
}
62+
// there are no other properties like destination tags
63+
if (destinationDetails.pathname === address) {
64+
return {
65+
address: address,
66+
destinationTag: undefined,
67+
};
68+
}
69+
70+
if (!destinationDetails.query) {
71+
throw new InvalidAddressError('no query params present');
72+
}
73+
74+
const queryDetails = querystring.parse(destinationDetails.query);
75+
if (!queryDetails.dt) {
76+
// if there are more properties, the query details need to contain the destination tag property.
77+
throw new InvalidAddressError('destination tag missing');
78+
}
79+
80+
if (Array.isArray(queryDetails.dt)) {
81+
// if queryDetails.dt is an array, that means dt was given multiple times, which is not valid
82+
throw new InvalidAddressError(
83+
`destination tag can appear at most once, but ${queryDetails.dt.length} destination tags were found`
84+
);
85+
}
86+
87+
const parsedTag = parseInt(queryDetails.dt, 10);
88+
if (!Number.isSafeInteger(parsedTag)) {
89+
throw new InvalidAddressError('invalid destination tag');
90+
}
91+
92+
if (parsedTag > 0xffffffff || parsedTag < 0) {
93+
throw new InvalidAddressError('destination tag out of range');
94+
}
95+
96+
return {
97+
address: destinationAddress,
98+
destinationTag: parsedTag,
99+
};
100+
}
101+
102+
/**
103+
* Construct a full, normalized address from an address and destination tag
104+
*/
105+
public normalizeAddress({ address, destinationTag }: Address): string {
106+
if (typeof address !== 'string') {
107+
throw new InvalidAddressError('invalid address, expected string');
108+
}
109+
if (typeof destinationTag === 'undefined' || destinationTag === null) {
110+
return address;
111+
}
112+
if (!Number.isInteger(destinationTag)) {
113+
throw new InvalidAddressError('invalid destination tag, expected integer');
114+
}
115+
if (destinationTag > 0xffffffff || destinationTag < 0) {
116+
throw new InvalidAddressError('destination tag out of range');
117+
}
118+
return `${address}?dt=${destinationTag}`;
119+
}
120+
121+
/**
122+
* @param message hex encoded string
123+
* @param privateKey
124+
* return hex encoded signature string, throws if any of the inputs are invalid
125+
*/
126+
public signString(message: string, privateKey: string): string {
127+
if (!this.isValidHex(message)) {
128+
throw new UtilsError('message must be a hex encoded string');
129+
}
130+
if (!this.isValidPrivateKey(privateKey)) {
131+
throw new UtilsError('invalid private key');
132+
}
133+
return rippleKeypairs.sign(message, privateKey);
134+
}
135+
136+
/**
137+
* @param message hex encoded string
138+
* @param signature hex encooded signature string
139+
* @param publicKey
140+
* return boolean, throws if any of the inputs are invalid
141+
*/
142+
public verifySignature(message: string, signature: string, publicKey: string): boolean {
143+
if (!this.isValidHex(message)) {
144+
throw new UtilsError('message must be a hex encoded string');
145+
}
146+
if (!this.isValidSignature(signature)) {
147+
throw new UtilsError('invalid signature');
148+
}
149+
if (!this.isValidPublicKey(publicKey)) {
150+
throw new UtilsError('invalid public key');
151+
}
152+
try {
153+
return rippleKeypairs.verify(message, signature, publicKey);
154+
} catch (e) {
155+
return false;
156+
}
157+
}
158+
}
159+
160+
const utils = new Utils();
161+
162+
export default utils;

modules/sdk-coin-xrp/src/txrp.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
* @format
55
*/
66
import { BaseCoin, BitGoBase } from '@bitgo/sdk-core';
7+
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
78
import { Xrp } from './xrp';
89

910
export class Txrp extends Xrp {
10-
protected constructor(bitgo: BitGoBase) {
11-
super(bitgo);
11+
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
12+
super(bitgo, staticsCoin);
1213
}
1314

14-
static createInstance(bitgo: BitGoBase): BaseCoin {
15-
return new Txrp(bitgo);
15+
static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
16+
return new Txrp(bitgo, staticsCoin);
1617
}
1718
/**
1819
* Identifier for the blockchain which supports this coin

0 commit comments

Comments
 (0)