Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/sdk-coin-xdc/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export { UploadKycBuilder, UploadKycCall } from './uploadKycBuilder';
export { Transaction, KeyPair } from '@bitgo/abstract-eth';
export { Utils };
export * from './validatorContract';
export { buildXdcKycMessage, signXdcKycMessage } from './xdcKycMessage';
export type { XdcKycMessageParams, XdcKycSignedMessage } from './xdcKycMessage';
52 changes: 52 additions & 0 deletions modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { IWallet, MessageStandardType } from '@bitgo/sdk-core';

export interface XdcKycMessageParams {
account: string; // wallet Ethereum address (checksummed)
timestamp: string; // ISO 8601, generated by caller at signing time
}

export interface XdcKycSignedMessage {
kycAccount: string;
kycMessage: string;
kycSignature: string;
}

/**
* Build the message string expected by the XinFin masternode IPFS API.
* Format: "[XDCmaster KYC <timestamp>] Upload KYC for <account>"
*/
export function buildXdcKycMessage(params: XdcKycMessageParams): string {
return `[XDCmaster KYC ${params.timestamp}] Upload KYC for ${params.account}`;
}

/**
* Sign the XDC KYC message using the wallet's TSS key.
* Calls wallet.signMessage() which routes through WP /msgrequests TSS ceremony.
*
* @param wallet - TSS wallet (must be XDC/TXDC)
* @param account - wallet Ethereum address
* @param walletPassphrase - user's wallet passphrase for TSS signing
* @returns signed message object ready to send as staking-service request fields
*/
export async function signXdcKycMessage(
wallet: IWallet,
account: string,
walletPassphrase: string
): Promise<XdcKycSignedMessage> {
const timestamp = new Date().toISOString();
const kycMessage = buildXdcKycMessage({ account, timestamp });

const signed = await wallet.signMessage({
message: {
messageRaw: kycMessage,
messageStandardType: MessageStandardType.EIP191,
},
walletPassphrase,
});

return {
kycAccount: account,
kycMessage,
kycSignature: signed.signature,
};
}
82 changes: 82 additions & 0 deletions modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as sinon from 'sinon';
import 'should';
import assert from 'assert';
import { IWallet, MessageStandardType } from '@bitgo/sdk-core';
import { buildXdcKycMessage, signXdcKycMessage } from '../../src/lib/xdcKycMessage';

const ACCOUNT = '0xAbCdEf1234567890AbCdEf1234567890AbCdEf12';
const TIMESTAMP = '2024-01-15T12:00:00.000Z';
const SIGNATURE = '0xdeadbeef';

describe('buildXdcKycMessage', function () {
it('should build the correct format string', function () {
const result = buildXdcKycMessage({ account: ACCOUNT, timestamp: TIMESTAMP });
result.should.equal(`[XDCmaster KYC ${TIMESTAMP}] Upload KYC for ${ACCOUNT}`);
});

it('should produce different strings for different timestamps', function () {
const result1 = buildXdcKycMessage({ account: ACCOUNT, timestamp: '2024-01-15T12:00:00.000Z' });
const result2 = buildXdcKycMessage({ account: ACCOUNT, timestamp: '2024-01-16T12:00:00.000Z' });
result1.should.not.equal(result2);
});
});

describe('signXdcKycMessage', function () {
let wallet: sinon.SinonStubbedInstance<IWallet>;

beforeEach(function () {
wallet = {
signMessage: sinon.stub(),
} as unknown as sinon.SinonStubbedInstance<IWallet>;
});

afterEach(function () {
sinon.restore();
});

it('should call wallet.signMessage with correct messageRaw and messageStandardType', async function () {
(wallet.signMessage as sinon.SinonStub).resolves({
txHash: '',
signature: SIGNATURE,
messageRaw: '',
});

await signXdcKycMessage(wallet as unknown as IWallet, ACCOUNT, 'passphrase');

sinon.assert.calledOnce(wallet.signMessage as sinon.SinonStub);
const call = (wallet.signMessage as sinon.SinonStub).getCall(0);
const params = call.args[0];
params.walletPassphrase.should.equal('passphrase');
params.message.messageStandardType.should.equal(MessageStandardType.EIP191);
params.message.messageRaw.should.startWith(`[XDCmaster KYC `);
params.message.messageRaw.should.containEql(`Upload KYC for ${ACCOUNT}`);
});

it('should return kycAccount, kycMessage, and kycSignature mapped correctly', async function () {
(wallet.signMessage as sinon.SinonStub).resolves({
txHash: '',
signature: SIGNATURE,
messageRaw: '',
});

const result = await signXdcKycMessage(wallet as unknown as IWallet, ACCOUNT, 'passphrase');

result.kycAccount.should.equal(ACCOUNT);
result.kycSignature.should.equal(SIGNATURE);
result.kycMessage.should.startWith('[XDCmaster KYC ');
result.kycMessage.should.containEql(`Upload KYC for ${ACCOUNT}`);
// kycMessage must match the messageRaw passed to signMessage
const passedMessageRaw = (wallet.signMessage as sinon.SinonStub).getCall(0).args[0].message.messageRaw;
result.kycMessage.should.equal(passedMessageRaw);
});

it('should propagate error if wallet.signMessage throws', async function () {
const signingError = new Error('TSS signing failed');
(wallet.signMessage as sinon.SinonStub).rejects(signingError);

await assert.rejects(
() => signXdcKycMessage(wallet as unknown as IWallet, ACCOUNT, 'passphrase'),
/TSS signing failed/
);
});
});
Loading