Skip to content

Commit 599334a

Browse files
committed
feat: implement CB58 encoding and decoding with checksum validation in Utils class
TICKET: WIN-7747
1 parent f9aba31 commit 599334a

File tree

3 files changed

+103
-53
lines changed

3 files changed

+103
-53
lines changed

modules/sdk-coin-flrp/src/lib/utils.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TransferableOutput } from '@flarenetwork/flarejs';
22
import { bech32 } from 'bech32';
3+
import bs58 from 'bs58';
34
import {
45
BaseUtils,
56
Entry,
@@ -30,6 +31,7 @@ import {
3031
PADSTART_CHAR,
3132
HEX_RADIX,
3233
STRING_TYPE,
34+
DECODED_BLOCK_ID_LENGTH,
3335
} from './constants';
3436

3537
// Regex utility functions for hex validation
@@ -91,8 +93,7 @@ export class Utils implements BaseUtils {
9193
let pubBuf: Buffer;
9294
if (pub.length === SHORT_PUB_KEY_LENGTH) {
9395
try {
94-
// For FlareJS, we'll need to implement CB58 decode functionality
95-
pubBuf = Buffer.from(pub, HEX_ENCODING); // Temporary placeholder
96+
pubBuf = this.cb58Decode(pub);
9697
} catch {
9798
return false;
9899
}
@@ -523,22 +524,6 @@ export class Utils implements BaseUtils {
523524
return parseInt(outputidx.toString(HEX_ENCODING), HEX_RADIX).toString();
524525
}
525526

526-
/**
527-
* CB58 decode function - simple Base58 decode implementation
528-
* @param {string} data - CB58 encoded string
529-
* @returns {Buffer} decoded buffer
530-
*/
531-
cb58Decode(data: string): Buffer {
532-
// For now, use a simple hex decode as placeholder
533-
// In a full implementation, this would be proper CB58 decoding
534-
try {
535-
return Buffer.from(data, HEX_ENCODING);
536-
} catch {
537-
// Fallback to buffer from string
538-
return Buffer.from(data);
539-
}
540-
}
541-
542527
/**
543528
* Convert string to bytes for FlareJS memo
544529
* Follows FlareJS utils.stringToBytes pattern
@@ -602,11 +587,60 @@ export class Utils implements BaseUtils {
602587
return memoBytes.length <= maxSize;
603588
}
604589

590+
/**
591+
* Adds a checksum to a Buffer and returns the concatenated result
592+
*/
593+
private addChecksum(buff: Buffer): Buffer {
594+
const hashslice = createHash('sha256').update(buff).digest().slice(28);
595+
return Buffer.concat([buff, hashslice]);
596+
}
597+
598+
/**
599+
* Validates a checksum on a Buffer and returns true if valid, false if not
600+
*/
601+
private validateChecksum(buff: Buffer): boolean {
602+
const checkslice = buff.slice(buff.length - 4);
603+
const hashslice = createHash('sha256')
604+
.update(buff.slice(0, buff.length - 4))
605+
.digest()
606+
.slice(28);
607+
return checkslice.toString('hex') === hashslice.toString('hex');
608+
}
609+
610+
/**
611+
* Encodes a Buffer as a base58 string with checksum
612+
*/
613+
public cb58Encode(bytes: Buffer): string {
614+
const withChecksum = this.addChecksum(bytes);
615+
return bs58.encode(withChecksum);
616+
}
617+
618+
/**
619+
* Decodes a base58 string with checksum to a Buffer
620+
*/
621+
public cb58Decode(str: string): Buffer {
622+
const decoded = bs58.decode(str);
623+
if (!this.validateChecksum(Buffer.from(decoded))) {
624+
throw new Error('Invalid checksum');
625+
}
626+
return Buffer.from(decoded.slice(0, decoded.length - 4));
627+
}
628+
629+
/**
630+
* Checks if a string is a valid CB58 (base58 with checksum) format
631+
*/
632+
private isCB58(str: string): boolean {
633+
try {
634+
this.cb58Decode(str);
635+
return true;
636+
} catch {
637+
return false;
638+
}
639+
}
640+
605641
isValidId(id: string): boolean {
606642
try {
607-
// For Flare P-chain, IDs are bech32 encoded
608-
const decoded = bech32.decode(id);
609-
return Buffer.from(bech32.fromWords(decoded.words)).length === 36;
643+
return this.isCB58(id) && this.cb58Decode(id).length === DECODED_BLOCK_ID_LENGTH;
610644
} catch {
611645
return false;
612646
}

modules/sdk-coin-flrp/test/resources/account.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export const SEED_ACCOUNT = {
88
'xpub661MyMwAqRbcGPTv9byjmWXD6LF1Tph7tzWXPbmh4Wt9TTmBvwTn2pYZZeQqnBEH6cTYCQEeYLLkvs7rwDN9cAKrK91rbBs8ixs532ZDZgE',
99
addressMainnet: 'P-flare1uyp5n76gjqltrddur7qlrsmt3kyh8fnrrqwtal',
1010
addressTestnet: 'P-costwo1uyp5n76gjqltrddur7qlrsmt3kyh8fnrmwhqk7',
11+
message: 'test message',
12+
signature:
13+
'1692c0a25c84d389e63692ca3b7ebc89835c3b11f6e64a505ddd71664d2a3ae914e0d1be13a824ae97331805650a0631df9ae92ba133f9aa81911f3a56807ba301',
1114
};
1215

1316
export const ACCOUNT_1 = {

modules/sdk-coin-flrp/test/unit/lib/utils.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { coins, FlareNetwork } from '@bitgo/statics';
22
import { NotImplementedError } from '@bitgo/sdk-core';
33
import * as assert from 'assert';
44
import { Utils } from '../../../src/lib/utils';
5+
import * as testData from '../../resources/account';
56

67
describe('Utils', function () {
78
let utils: Utils;
@@ -12,28 +13,32 @@ describe('Utils', function () {
1213

1314
describe('includeIn', function () {
1415
it('should return true when all wallet addresses are in output addresses', function () {
15-
const walletAddresses = ['addr1', 'addr2'];
16-
const outputAddresses = ['addr1', 'addr2', 'addr3'];
16+
const walletAddresses = [testData.ACCOUNT_1.addressMainnet, testData.ACCOUNT_3.address];
17+
const outputAddresses = [
18+
testData.ACCOUNT_1.addressMainnet,
19+
testData.ACCOUNT_3.address,
20+
testData.ACCOUNT_4.address,
21+
];
1722

1823
assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), true);
1924
});
2025

2126
it('should return false when not all wallet addresses are in output addresses', function () {
22-
const walletAddresses = ['addr1', 'addr2'];
23-
const outputAddresses = ['addr1', 'addr3'];
27+
const walletAddresses = [testData.ACCOUNT_1.addressMainnet, testData.ACCOUNT_3.address];
28+
const outputAddresses = [testData.ACCOUNT_3.address, testData.ACCOUNT_4.address];
2429

2530
assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), false);
2631
});
2732

2833
it('should return true for empty wallet addresses', function () {
2934
const walletAddresses: string[] = [];
30-
const outputAddresses = ['addr1', 'addr2'];
35+
const outputAddresses = [testData.ACCOUNT_1.addressMainnet, testData.ACCOUNT_3.address];
3136

3237
assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), true);
3338
});
3439

3540
it('should return false when wallet address not found in empty output addresses', function () {
36-
const walletAddresses = ['addr1'];
41+
const walletAddresses = [testData.ACCOUNT_1.addressMainnet];
3742
const outputAddresses: string[] = [];
3843

3944
assert.strictEqual(utils.includeIn(walletAddresses, outputAddresses), false);
@@ -42,37 +47,44 @@ describe('Utils', function () {
4247

4348
describe('isValidAddress', function () {
4449
it('should validate single valid Flare addresses', function () {
45-
// Flare addresses start with 'flare:' or 'C-flare:'
4650
const validAddresses = [
47-
'flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh',
48-
'C-flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh',
51+
testData.SEED_ACCOUNT.addressMainnet,
52+
testData.SEED_ACCOUNT.addressTestnet,
53+
testData.ACCOUNT_1.addressMainnet,
54+
testData.ACCOUNT_1.addressTestnet,
4955
];
5056

5157
validAddresses.forEach((addr) => {
52-
// Note: The current implementation uses regex validation
53-
// This test will be updated once proper Flare address validation is implemented
5458
const result = utils.isValidAddress(addr);
55-
// Currently returns false due to placeholder implementation
5659
assert.strictEqual(typeof result, 'boolean');
60+
assert.strictEqual(result, true);
5761
});
5862
});
5963

6064
it('should validate array of addresses', function () {
6165
const addresses = [
62-
'flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh',
63-
'flare1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6f4avh',
66+
testData.SEED_ACCOUNT.addressMainnet,
67+
testData.SEED_ACCOUNT.addressTestnet,
68+
testData.ACCOUNT_1.addressMainnet,
69+
testData.ACCOUNT_1.addressTestnet,
6470
];
6571

6672
const result = utils.isValidAddress(addresses);
6773
assert.strictEqual(typeof result, 'boolean');
74+
assert.strictEqual(result, true);
6875
});
6976

7077
it('should validate addresses separated by ~', function () {
7178
const addressString =
72-
'flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh~flare1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6f4avh';
79+
testData.SEED_ACCOUNT.addressTestnet +
80+
'~' +
81+
testData.ACCOUNT_1.addressTestnet +
82+
'~' +
83+
testData.ACCOUNT_4.address;
7384

7485
const result = utils.isValidAddress(addressString);
7586
assert.strictEqual(typeof result, 'boolean');
87+
assert.strictEqual(result, true);
7688
});
7789

7890
it('should reject obviously invalid addresses', function () {
@@ -86,17 +98,18 @@ describe('Utils', function () {
8698

8799
invalidAddresses.forEach((addr) => {
88100
const result = utils.isValidAddress(addr);
89-
// Current implementation may not catch all invalid addresses
90101
assert.strictEqual(typeof result, 'boolean');
102+
assert.strictEqual(result, false);
91103
});
92104
});
93105
});
94106

95107
describe('isValidAddressRegex', function () {
96108
it('should test address format with regex', function () {
97-
const testAddress = 'flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh';
109+
const testAddress = testData.SEED_ACCOUNT.addressTestnet;
98110
const result = utils['isValidAddressRegex'](testAddress);
99111
assert.strictEqual(typeof result, 'boolean');
112+
assert.strictEqual(result, true);
100113
});
101114

102115
it('should reject empty strings', function () {
@@ -108,9 +121,9 @@ describe('Utils', function () {
108121
describe('isValidTransactionId', function () {
109122
it('should return true for valid transaction IDs', function () {
110123
const validTxIds = [
111-
'UALK7W31ohV7z4sQ3W6XFasTQQZUDdHMp3ZsXkou3Zdu2EKgn',
112-
'2fRj2y8SEv85VXRBfVYv9Uw8UDr1vEdrxFk9rBF5jU9fj3XaeP',
113-
'21W2bYubjrBdZe8taKvC2KDGJutAdgyFbVgzJ9C9ygm4SjGcWz',
124+
'6wewzpFrTDPGmFfRJoT9YyGVxsRDxQXu6pz6LSXLf2eU6StBe',
125+
'3SuMRBREQwhsR1qQYjSpHPNgwV7keXQbKBgP8jULnKdz7ppEV',
126+
'2ExGh7o1c4gQtQrzDt2BvJxg42FswGWaLY7NEXCqcejPxjSTij',
114127
];
115128

116129
validTxIds.forEach((txId) => {
@@ -138,9 +151,9 @@ describe('Utils', function () {
138151
describe('isValidBlockId', function () {
139152
it('should return true for valid block IDs', function () {
140153
const validTxIds = [
141-
'2nf4RxA6mHVTEgfzUdGsS1XAoPbsz817fWKyVFwN8Hh3C41XuX',
142-
'22Dvn8QXXRc8FwZ5ke4X4BLTFhSbyDXC4HpNJGk9snmD1Bu6n3',
143-
'2uTF5zFSHTGWNdMBKe1AWjNojGY7sxdssqR9gxAZAJYf9PxNrd',
154+
'mg3B2HsQ8Pqe63J2arXi6uD3wGJV1fgCNe5bRufDToAgVRVBp',
155+
'rVWodN2iTugUMckkgf8ntXcoyuduey24ZgXCMi66mrFegcV4R',
156+
'2MrU9G74ra9QX99wQRxvKrbzV93i6Ua7KgHMETVMSYoJq2tb5g',
144157
];
145158

146159
validTxIds.forEach((txId) => {
@@ -178,20 +191,20 @@ describe('Utils', function () {
178191
describe('createSignature', function () {
179192
it('should create signature using secp256k1', function () {
180193
const network = coins.get('flrp').network as FlareNetwork;
181-
const message = Buffer.from('hello world', 'utf8');
182-
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
194+
const message = Buffer.from(testData.SEED_ACCOUNT.message, 'utf8');
195+
const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex');
183196

184-
const signature = utils.createSignature(network, message, privateKey);
197+
const signature = utils.createSignature(network, message, privateKey).toString('hex');
185198

186-
assert.ok(signature instanceof Buffer);
187199
assert.ok(signature.length > 0);
200+
assert.strictEqual(signature, testData.SEED_ACCOUNT.signature);
188201
});
189202

190203
it('should create different signatures for different messages', function () {
191204
const network = coins.get('flrp').network as FlareNetwork;
192205
const message1 = Buffer.from('message 1', 'utf8');
193206
const message2 = Buffer.from('message 2', 'utf8');
194-
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
207+
const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex');
195208

196209
const sig1 = utils.createSignature(network, message1, privateKey);
197210
const sig2 = utils.createSignature(network, message2, privateKey);
@@ -211,23 +224,23 @@ describe('Utils', function () {
211224
describe('verifySignature', function () {
212225
it('should verify valid signature', function () {
213226
const network = coins.get('flrp').network as FlareNetwork;
214-
const message = Buffer.from('hello world', 'utf8');
215-
const privateKey = Buffer.from('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 'hex');
227+
const message = Buffer.from(testData.SEED_ACCOUNT.message, 'utf8');
228+
const privateKey = Buffer.from(testData.SEED_ACCOUNT.privateKey, 'hex');
216229

217230
// Create signature
218231
const signature = utils.createSignature(network, message, privateKey);
219232

220233
// Get public key (this would normally come from the private key)
221234
// For testing, we'll use a mock public key approach
222-
const publicKey = Buffer.from('02' + '0'.repeat(62), 'hex'); // Compressed public key format
235+
const publicKey = Buffer.from(testData.SEED_ACCOUNT.publicKey, 'hex'); // Compressed public key format
223236

224237
// Note: This test may fail if the public key doesn't match the private key
225238
// In a real implementation, you'd derive the public key from the private key
226239
// The method returns false when verification fails instead of throwing
227240
const isValid = utils.verifySignature(network, message, signature, publicKey);
228241
assert.strictEqual(typeof isValid, 'boolean');
229242
// With mock public key, this should return false
230-
assert.strictEqual(isValid, false);
243+
assert.strictEqual(isValid, true);
231244
});
232245

233246
it('should return false for invalid signature', function () {

0 commit comments

Comments
 (0)