Skip to content

Commit 32b3ebf

Browse files
chore: unit tests and credential change
TICKET: WIN-6322
1 parent 4aefc76 commit 32b3ebf

File tree

8 files changed

+690
-26
lines changed

8 files changed

+690
-26
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from '@bitgo/sdk-core';
3636
import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics';
3737
import { Hash } from 'crypto';
38+
import { KeyPair as FlrpKeyPair } from './lib/keyPair';
3839

3940
export class Flrp extends BaseCoin {
4041
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -93,7 +94,15 @@ export class Flrp extends BaseCoin {
9394

9495
// Key methods (stubs)
9596
generateKeyPair(): KeyPair {
96-
throw new Error('generateKeyPair not implemented');
97+
const keyPair = new FlrpKeyPair();
98+
const keys = keyPair.getKeys();
99+
if (!keys.prv) {
100+
throw new Error('Failed to generate private key');
101+
}
102+
return {
103+
pub: keys.pub,
104+
prv: keys.prv,
105+
};
97106
}
98107
generateRootKeyPair(): KeyPair {
99108
throw new Error('generateRootKeyPair not implemented');

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

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { BaseCoin as CoinConfig } from '@bitgo/statics';
22
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
3+
import { Credential, Signature } from '@flarenetwork/flarejs';
4+
5+
// Constants for signature handling
6+
const SECP256K1_SIGNATURE_LENGTH = 65;
37

48
/**
5-
* Minimal placeholder for Flare P-chain atomic transaction building.
6-
* This will be expanded with proper Flare P-chain logic (inputs/outputs/credentials, UTXO handling, fees, etc.).
9+
* Flare P-chain atomic transaction builder with FlareJS credential support.
10+
* This provides the foundation for building Flare P-chain transactions with proper
11+
* credential handling using FlareJS Credential and Signature classes.
712
*/
813
export abstract class AtomicTransactionBuilder {
914
protected readonly _coinConfig: Readonly<CoinConfig>;
@@ -53,13 +58,88 @@ export abstract class AtomicTransactionBuilder {
5358
}
5459
}
5560

61+
/**
62+
* Validates that credentials array is properly formed
63+
* @param credentials - Array of credentials to validate
64+
*/
65+
protected validateCredentials(credentials: Credential[]): void {
66+
if (!Array.isArray(credentials)) {
67+
throw new BuildTransactionError('Credentials must be an array');
68+
}
69+
70+
credentials.forEach((credential, index) => {
71+
if (!(credential instanceof Credential)) {
72+
throw new BuildTransactionError(`Invalid credential at index ${index}`);
73+
}
74+
});
75+
}
76+
5677
/**
5778
* Placeholder that should assemble inputs/outputs and credentials once UTXO + key logic is implemented.
5879
*/
59-
protected createInputOutput(_total: bigint): { inputs: unknown[]; outputs: unknown[]; credentials: unknown[] } {
80+
protected createInputOutput(_total: bigint): { inputs: unknown[]; outputs: unknown[]; credentials: Credential[] } {
6081
return { inputs: [], outputs: [], credentials: [] };
6182
}
6283

84+
/**
85+
* Flare equivalent of Avalanche's SelectCredentialClass
86+
* Creates a credential with the provided signatures
87+
*
88+
* @param credentialId - The credential ID (not used in FlareJS but kept for compatibility)
89+
* @param signatures - Array of signature hex strings or empty strings for placeholders
90+
* @returns Credential instance
91+
*/
92+
protected createFlareCredential(_credentialId: number, signatures: string[]): Credential {
93+
if (!Array.isArray(signatures)) {
94+
throw new BuildTransactionError('Signatures must be an array');
95+
}
96+
97+
if (signatures.length === 0) {
98+
throw new BuildTransactionError('Signatures array cannot be empty');
99+
}
100+
101+
const sigs = signatures.map((sig, index) => {
102+
// Handle empty/placeholder signatures
103+
if (!sig || sig.length === 0) {
104+
return new Signature(new Uint8Array(SECP256K1_SIGNATURE_LENGTH));
105+
}
106+
107+
// Validate hex string format
108+
const cleanSig = sig.startsWith('0x') ? sig.slice(2) : sig;
109+
if (!/^[0-9a-fA-F]*$/.test(cleanSig)) {
110+
throw new BuildTransactionError(`Invalid hex signature at index ${index}: contains non-hex characters`);
111+
}
112+
113+
// Convert to buffer and validate length
114+
const sigBuffer = Buffer.from(cleanSig, 'hex');
115+
if (sigBuffer.length > SECP256K1_SIGNATURE_LENGTH) {
116+
throw new BuildTransactionError(
117+
`Signature too long at index ${index}: ${sigBuffer.length} bytes (max ${SECP256K1_SIGNATURE_LENGTH})`
118+
);
119+
}
120+
121+
// Create fixed-length buffer and copy signature data
122+
const fixedLengthBuffer = Buffer.alloc(SECP256K1_SIGNATURE_LENGTH);
123+
sigBuffer.copy(fixedLengthBuffer);
124+
125+
try {
126+
return new Signature(new Uint8Array(fixedLengthBuffer));
127+
} catch (error) {
128+
throw new BuildTransactionError(
129+
`Failed to create signature at index ${index}: ${error instanceof Error ? error.message : 'unknown error'}`
130+
);
131+
}
132+
});
133+
134+
try {
135+
return new Credential(sigs);
136+
} catch (error) {
137+
throw new BuildTransactionError(
138+
`Failed to create credential: ${error instanceof Error ? error.message : 'unknown error'}`
139+
);
140+
}
141+
}
142+
63143
/**
64144
* Base initBuilder used by concrete builders. For now just returns this so fluent API works.
65145
*/

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,24 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
143143
* @protected
144144
*/
145145
protected buildFlareTransaction(): void {
146-
if (this.transaction.hasCredentials) return; // placeholder: credentials not yet implemented
147-
if (this._amount === undefined) throw new Error('amount is required');
148-
if (this.transaction._fromAddresses.length !== 1) throw new Error('sender is one and required');
149-
if (this.transaction._to.length === 0) throw new Error('to is required');
150-
if (!this.transaction._fee.feeRate) throw new Error('fee rate is required');
151-
if (this._nonce === undefined) throw new Error('nonce is required');
146+
if (this.transaction.hasCredentials) {
147+
return; // placeholder: credentials not yet implemented
148+
}
149+
if (this._amount === undefined) {
150+
throw new Error('amount is required');
151+
}
152+
if (this.transaction._fromAddresses.length !== 1) {
153+
throw new Error('sender is one and required');
154+
}
155+
if (this.transaction._to.length === 0) {
156+
throw new Error('to is required');
157+
}
158+
if (!this.transaction._fee.feeRate) {
159+
throw new Error('fee rate is required');
160+
}
161+
if (this._nonce === undefined) {
162+
throw new Error('nonce is required');
163+
}
152164

153165
// Compose placeholder unsigned tx shape
154166
const feeRate = BigInt(this.transaction._fee.feeRate);
Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { coins } from '@bitgo/statics';
2+
import * as assert from 'assert';
3+
import { Flrp } from '../../src/flrp';
14
import { BitGoBase } from '@bitgo/sdk-core';
25
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
36
import { BitGoAPI } from '@bitgo/sdk-api';
4-
import { Flrp } from '../../src/flrp';
5-
import { coins } from '@bitgo/statics';
67

78
describe('Flrp', function () {
89
let bitgo: TestBitGoAPI;
10+
const staticsCoin = coins.get('flrp');
911

1012
before(function () {
1113
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
@@ -17,18 +19,85 @@ describe('Flrp', function () {
1719
);
1820
});
1921

20-
it('createInstance returns a Flrp instance', function () {
21-
const staticsCoin = coins.get('flrp');
22-
const coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin);
23-
coin.should.be.instanceOf(Flrp);
22+
describe('createInstance', function () {
23+
it('should return a Flrp instance', function () {
24+
const coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin);
25+
assert.ok(coin instanceof Flrp);
26+
});
27+
28+
it('should produce distinct objects on multiple calls', function () {
29+
const a = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin);
30+
const b = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin);
31+
assert.notStrictEqual(a, b);
32+
assert.ok(a instanceof Flrp);
33+
assert.ok(b instanceof Flrp);
34+
});
2435
});
2536

26-
it('multiple createInstance calls produce distinct objects', function () {
27-
const sc = coins.get('flrp');
28-
const a = Flrp.createInstance(bitgo as unknown as BitGoBase, sc);
29-
const b = Flrp.createInstance(bitgo as unknown as BitGoBase, sc);
30-
a.should.not.equal(b);
31-
a.should.be.instanceOf(Flrp);
32-
b.should.be.instanceOf(Flrp);
37+
describe('coin properties', function () {
38+
let coin: Flrp;
39+
40+
beforeEach(function () {
41+
coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin) as Flrp;
42+
});
43+
44+
it('should have correct coin family', function () {
45+
assert.strictEqual(coin.getFamily(), staticsCoin.family);
46+
});
47+
48+
it('should have correct coin name', function () {
49+
assert.strictEqual(coin.getFullName(), staticsCoin.fullName);
50+
});
51+
52+
it('should have correct base factor', function () {
53+
assert.strictEqual(coin.getBaseFactor(), Math.pow(10, staticsCoin.decimalPlaces));
54+
});
55+
56+
it('should validate addresses using utils', function () {
57+
const validAddress = 'flare1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6f4avh';
58+
const result = coin.isValidAddress(validAddress);
59+
assert.strictEqual(typeof result, 'boolean');
60+
});
61+
62+
it('should generate key pairs', function () {
63+
const keyPair = coin.generateKeyPair();
64+
assert.ok('pub' in keyPair);
65+
assert.ok('prv' in keyPair);
66+
if (keyPair.pub && keyPair.prv) {
67+
assert.strictEqual(typeof keyPair.pub, 'string');
68+
assert.strictEqual(typeof keyPair.prv, 'string');
69+
}
70+
});
71+
});
72+
73+
describe('error handling', function () {
74+
it('should handle construction with invalid parameters', function () {
75+
assert.throws(() => Flrp.createInstance(null as unknown as BitGoBase, staticsCoin));
76+
assert.throws(() => Flrp.createInstance(bitgo as unknown as BitGoBase, null as unknown as typeof staticsCoin));
77+
});
78+
});
79+
80+
describe('inheritance and methods', function () {
81+
let coin: Flrp;
82+
83+
beforeEach(function () {
84+
coin = Flrp.createInstance(bitgo as unknown as BitGoBase, staticsCoin) as Flrp;
85+
});
86+
87+
it('should have required base coin methods', function () {
88+
assert.ok('getFamily' in coin);
89+
assert.ok('getFullName' in coin);
90+
assert.ok('getBaseFactor' in coin);
91+
assert.ok('isValidAddress' in coin);
92+
assert.ok('generateKeyPair' in coin);
93+
});
94+
95+
it('should handle address validation consistently', function () {
96+
const validAddress = 'flare1test';
97+
const invalidAddress = 'invalid-address';
98+
99+
assert.strictEqual(typeof coin.isValidAddress(validAddress), 'boolean');
100+
assert.strictEqual(typeof coin.isValidAddress(invalidAddress), 'boolean');
101+
});
33102
});
34103
});

0 commit comments

Comments
 (0)