From e4729d14f9060971246004de7a6ba992391b43f0 Mon Sep 17 00:00:00 2001 From: Doddanna B Date: Mon, 29 Jun 2026 13:21:52 +0000 Subject: [PATCH] feat(sdk-coin-xdc): add signXdcKycMessage utility Add signXdcKycMessage and buildXdcKycMessage as standalone utility functions in modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts. The XinFin masternode IPFS API (/api/ipfs/addKYC) now requires EIP-191 signed message headers to prove wallet ownership. The message format is "[XDCmaster KYC ] Upload KYC for ". signXdcKycMessage generates a fresh timestamp, builds the canonical message string, and signs it via wallet.signMessage() with MessageStandardType.EIP191, which routes through the WP /msgrequests TSS ceremony. Returns kycAccount, kycMessage, and kycSignature ready to send as staking-service request fields. Unit tests cover: correct format string, timestamp non-determinism, correct signMessage call params, correct return value mapping, and error propagation when signing fails. Ticket: SI-921 Session-Id: c8a118ce-e817-497d-b221-aa0d0dccba73 Task-Id: 400c947f-58b8-40eb-a364-e807f68fd299 --- modules/sdk-coin-xdc/src/lib/index.ts | 2 + modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts | 52 ++++++++++++ .../sdk-coin-xdc/test/unit/xdcKycMessage.ts | 82 +++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts create mode 100644 modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts diff --git a/modules/sdk-coin-xdc/src/lib/index.ts b/modules/sdk-coin-xdc/src/lib/index.ts index 6740a21455..a213df19de 100644 --- a/modules/sdk-coin-xdc/src/lib/index.ts +++ b/modules/sdk-coin-xdc/src/lib/index.ts @@ -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'; diff --git a/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts b/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts new file mode 100644 index 0000000000..3dbece732a --- /dev/null +++ b/modules/sdk-coin-xdc/src/lib/xdcKycMessage.ts @@ -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 ] Upload KYC for " + */ +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 { + 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, + }; +} diff --git a/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts b/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts new file mode 100644 index 0000000000..6cb93f2397 --- /dev/null +++ b/modules/sdk-coin-xdc/test/unit/xdcKycMessage.ts @@ -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; + + beforeEach(function () { + wallet = { + signMessage: sinon.stub(), + } as unknown as sinon.SinonStubbedInstance; + }); + + 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/ + ); + }); +});